
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 라우트는 조회 결과를 반환하기 전에 동일한 접근 제어 결정을 강제하지 않습니다.
이 랩은 두 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 | 127.0.0.1:8002 -> 8000 |