此仓库包含一个本地 Docker 实验环境,用于复现 CVE-2026-46645,这是一个影响 SQLAdmin 的 ajax_lookup 端点的授权绕过漏洞。
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
本实验室特意使用简单的 `Report` / `SecretProject` 数据模型,以便于理解授权绕过问题。这些模型名称并非漏洞的根源,仅用于创建可控的复现条件。
本实验室仅用于可控的本地研究、源码级理解以及作品集展示。
## Verified Facts
| 声明 | 证据 | 如何在实验室中验证 |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| 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`,保留会话 cookie,然后请求 `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 部署模式。它有意创建了一个小型 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
## 源码补丁摘要
有意义的 upstream 补丁是对 `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 |
该应用创建了两个 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"}
如果需要,在浏览器中打开管理界面:```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