299 lines
12 KiB
Markdown
299 lines
12 KiB
Markdown
|
|
# 缺陷单:调试端点(/test-ping /test-error /version)生产无门控暴露
|
|||
|
|
|
|||
|
|
> **缺陷编号**: BUG-安全-005
|
|||
|
|
> **标题简称**: 调试端点无门控
|
|||
|
|
> **P0 序列**: P0-NEW12(连续命名 P0-NEW11 = openapi/docs 按 env 切)
|
|||
|
|
> **版本**: v1.0
|
|||
|
|
> **状态**: [已修复]
|
|||
|
|
> **优先级**: P0-High
|
|||
|
|
> **发现日期**: 2026-08-12
|
|||
|
|
> **发现人**: 宋献(jumpserver-V2 容器内 python urllib 绕开 nginx 直测 FastAPI)
|
|||
|
|
> **指派人**: 宋献
|
|||
|
|
> **修复人**: Duckula(AI 助手)
|
|||
|
|
> **修复日期**: 2026-08-12
|
|||
|
|
> **关闭日期**: 2026-08-12
|
|||
|
|
> **处理方式**: 主理人直接修复
|
|||
|
|
> **关联需求**: 安全治理(隐含于系统设计)
|
|||
|
|
> **关联文档**:
|
|||
|
|
> - 测试用例: `tests/test_debug_endpoints_gating.py`(规划中)
|
|||
|
|
> - 项目记忆: `.workbuddy/memory/2026-08-12.md`
|
|||
|
|
> - Git commit: 待推送
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. 基本信息
|
|||
|
|
|
|||
|
|
| 字段 | 内容 |
|
|||
|
|
|------|------|
|
|||
|
|
| 缺陷标题 | FastAPI 调试端点(/test-ping /test-error /version)在生产环境无任何门控,攻击者绕过 nginx 即可调用 |
|
|||
|
|
| 影响范围 | 预生产服 itsupport.servyou.com.cn(10.90.5.110)+ 任何部署该镜像的环境(容器间网络可达 backend:8000 即被攻击) |
|
|||
|
|
| 涉及模块 | 后端主入口(app/main.py)+ 调试端点 |
|
|||
|
|
| 涉及文件 | `src/backend/app/main.py`(line 975-983 旧 /test-ping/test-error、line 1052-1076 旧 /version)<br>新增: `src/backend/app/api/debug.py`(集中管理) |
|
|||
|
|
| 触发条件 | 1. 容器间网络可达 backend:8000(如 docker network wecom_it_default 内其他容器,nginx 失效时的公网直连)<br>2. 攻击者通过 `python urllib` 绕开 nginx 直接访问 backend:8000<br>3. /test-ping 200 pong(暴露后端存活)/test-error 200 "服务器内部错误"(暴露异常处理路径)/version 200 {version:"1.1.0"}(暴露 git hash 可锁定已知漏洞) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 漏洞证据
|
|||
|
|
|
|||
|
|
### 2.1 漏洞扫描结果(2026-08-12 jumpserver-V2 容器内直测)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# jumpserver-V2 容器内 python urllib 绕开 nginx 直测 FastAPI
|
|||
|
|
import urllib.request
|
|||
|
|
|
|||
|
|
for ep in ['/test-ping', '/test-error', '/version', '/openapi.json']:
|
|||
|
|
try:
|
|||
|
|
resp = urllib.request.urlopen(f'http://backend:8000{ep}', timeout=5)
|
|||
|
|
body = resp.read().decode()[:200]
|
|||
|
|
print(f'{ep:20s} {resp.status} {body}')
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f'{ep:20s} ERR {e}')
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
实测输出:
|
|||
|
|
|
|||
|
|
| 端点 | 状态 | 响应 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `/test-ping` | 200 | `{"code":0,"data":{"message":"pong"},"message":"success"}` |
|
|||
|
|
| `/test-error` | 200 | `{"code":1005,"data":null,"message":"服务器内部错误,请稍后重试或联系管理员"}` |
|
|||
|
|
| `/version` | 200 | `{"service":"wecom-it-smart-desk","version":"1.1.0","build":"abc12345"}` |
|
|||
|
|
| `/openapi.json` | 404 | (P0-NEW11 已修 ✅) |
|
|||
|
|
|
|||
|
|
### 2.2 风险分析
|
|||
|
|
|
|||
|
|
| 端点 | 风险 | 攻击向量 |
|
|||
|
|
|------|------|---------|
|
|||
|
|
| `/test-ping` | 中 — 暴露后端存活(已可用 /health 推断) | 端口扫描后存活探测 |
|
|||
|
|
| `/test-error` | 中 — 暴露异常处理路径(虽然已 200 兜底,但暴露代码结构) | 攻击者验证异常处理逻辑 |
|
|||
|
|
| `/version` | **高** — 暴露 git build hash,攻击者可锁定已知漏洞精确打击 | git hash → 关联 commit → 查 CVE 数据库 |
|
|||
|
|
| `/openapi.json` | 已修 | — |
|
|||
|
|
|
|||
|
|
### 2.3 攻击场景
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
场景 A — nginx 配置被误改/失效:
|
|||
|
|
公网 → nginx 502/404 → 攻击者尝试 backend:8000 直连 → 拿到 3 个调试端点
|
|||
|
|
|
|||
|
|
场景 B — 容器间网络可达:
|
|||
|
|
jumpserver-V2 容器(10.x.x.x)→ docker network wecom_it_default → backend:8000
|
|||
|
|
→ python urllib 直连 → 拿到 3 个调试端点
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 根因分析
|
|||
|
|
|
|||
|
|
| 根因 | 位置 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| **R1: 端点散落无模块化** | `main.py:975-983 / 1052-1076` | 调试端点直接以 `@app.get()` 形式写在主入口,无独立 router |
|
|||
|
|
| **R2: 端点内无门控** | 三个端点函数体 | 完全没有任何 `_is_dev_mode()` / `_dev_mode_enabled()` 校验 |
|
|||
|
|
| **R3: 挂载层无门控** | main.py create_app() | 调试端点无条件挂载到生产 app,跟业务路由无差别 |
|
|||
|
|
| **R4: docs_url 切了但调试端点漏切** | main.py:796-798 | P0-NEW11 已修 docs_url/redoc_url/openapi_url,但调试端点是另一回事 |
|
|||
|
|
|
|||
|
|
**设计缺陷**:
|
|||
|
|
- 调试端点本应遵循"仅 dev 模式可见"原则(参考 `dev_auth.py` 的 `_dev_mode_enabled()` 二次校验)
|
|||
|
|
- 实际却以"生产同等待遇"挂载,违反最小权限原则
|
|||
|
|
- 同模块的 `dev_auth_router`(`/api/dev/login` 等)已有双重门控,调试端点却裸奔——是历史遗漏
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. 修复方案
|
|||
|
|
|
|||
|
|
### 4.1 方案选择
|
|||
|
|
|
|||
|
|
采用用户建议方案(**最小变更 + 双重门控 + 单一真源**):
|
|||
|
|
|
|||
|
|
| 步骤 | 内容 | 优势 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| ① 新建 `app/api/debug.py` | 集中 3 个调试端点(ping/error/version) | 模块化,便于未来扩展 |
|
|||
|
|
| ② 双重门控 | (a) main.py 在 `if _is_dev_mode():` 块内 include_router<br>(b) 每个端点内部再 `_is_dev_mode()` 二次校验 | 防挂载顺序错乱 / 中间件失败 |
|
|||
|
|
| ③ 单一真源 | debug.py 的 `_is_dev_mode()` 委托给 main.py:48 的实现,避免三处定义漂移 | 防止后续 dev_auth / main / debug 三份独立判定代码不一致 |
|
|||
|
|
| ④ FastAPI 实例化按 env 切 docs_url | P0-NEW11 已实现 ✅ | 不动 |
|
|||
|
|
|
|||
|
|
### 4.2 修复代码
|
|||
|
|
|
|||
|
|
#### 4.2.1 新增 `src/backend/app/api/debug.py`
|
|||
|
|
|
|||
|
|
详见 `src/backend/app/api/debug.py`,核心结构:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
router = APIRouter(tags=["debug"])
|
|||
|
|
|
|||
|
|
@router.get("/debug/ping")
|
|||
|
|
async def debug_ping():
|
|||
|
|
if not _is_dev_mode():
|
|||
|
|
raise HTTPException(status_code=403, detail="Debug endpoints are only available in DEV mode.")
|
|||
|
|
return success_response(data={"message": "pong"})
|
|||
|
|
|
|||
|
|
# /debug/error / /debug/version 同款门控
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.2.2 修改 `src/backend/app/main.py`
|
|||
|
|
|
|||
|
|
**删除旧端点**(line 975-983 / 1052-1076):
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 删除
|
|||
|
|
@app.get("/test-ping", tags=["诊断"])
|
|||
|
|
async def test_ping(): ...
|
|||
|
|
@app.get("/test-error", tags=["诊断"])
|
|||
|
|
async def test_error(): ...
|
|||
|
|
@app.get("/version", tags=["系统"])
|
|||
|
|
async def version(): ...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**挂载新 router**(在 dev_auth_router 旁边):
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
if _is_dev_mode():
|
|||
|
|
from app.api.dev_auth import router as dev_auth_router
|
|||
|
|
app.include_router(dev_auth_router)
|
|||
|
|
# P0-NEW12
|
|||
|
|
from app.api.debug import router as debug_router
|
|||
|
|
app.include_router(debug_router)
|
|||
|
|
# ...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**清理无用 import**(`success_response` 不再使用):
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 改前
|
|||
|
|
from app.utils.response import AppException, app_exception_handler, success_response
|
|||
|
|
# 改后
|
|||
|
|
from app.utils.response import AppException, app_exception_handler
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. 验证
|
|||
|
|
|
|||
|
|
### 5.1 静态检查
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
$ python -c "import ast; ast.parse(open('app/main.py').read()); print('main.py OK')"
|
|||
|
|
main.py OK
|
|||
|
|
$ python -c "import ast; ast.parse(open('app/api/debug.py').read()); print('debug.py OK')"
|
|||
|
|
debug.py OK
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.2 路由扫描(DEV 模式)
|
|||
|
|
|
|||
|
|
重启 backend,扫描路由表:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 预期(DEV_MODE=true)
|
|||
|
|
✓ /api/debug/ping — module 加载
|
|||
|
|
✓ /api/debug/error — module 加载
|
|||
|
|
✓ /api/debug/version — module 加载
|
|||
|
|
✓ /api/dev/login — 已有
|
|||
|
|
✓ /api/dev/users — 已有
|
|||
|
|
✓ /api/dev/health — 已有
|
|||
|
|
✓ /health / /ready / /metrics — 保留
|
|||
|
|
|
|||
|
|
# 预期(DEV_MODE=false/未设)
|
|||
|
|
✗ /api/debug/* — 端点根本不挂载
|
|||
|
|
✗ /api/dev/* — 端点根本不挂载
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.3 端点门控测试
|
|||
|
|
|
|||
|
|
#### 5.3.1 生产模式(DEV_MODE 未设)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 期望全部 403
|
|||
|
|
GET /api/debug/ping → 403 Not Found(甚至路由不存在)
|
|||
|
|
GET /api/debug/error → 404(路由不存在)
|
|||
|
|
GET /api/debug/version → 404(路由不存在)
|
|||
|
|
GET /test-ping → 404(路由不存在,已删除)
|
|||
|
|
GET /test-error → 404(路由不存在,已删除)
|
|||
|
|
GET /version → 404(路由不存在,已删除)
|
|||
|
|
GET /openapi.json → 404(P0-NEW11 已修)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5.3.2 开发模式(DEV_MODE=true)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 期望全部 200
|
|||
|
|
GET /api/debug/ping → 200 {"code":0,"data":{"message":"pong"}}
|
|||
|
|
GET /api/debug/version → 200 {"service":"wecom-it-smart-desk","version":"1.1.0","build":"abc12345",...}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5.3.3 双门控异常路径测试
|
|||
|
|
|
|||
|
|
即便 main.py 挂载逻辑有 bug,debug.py 端点内部 _is_dev_mode() 二次校验也应兜底 403(防 fail-open)。
|
|||
|
|
|
|||
|
|
### 5.4 公网复测(部署后)
|
|||
|
|
|
|||
|
|
通过 jumpserver-V2 容器绕开 nginx 直测 backend:8000:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
python -c "import urllib.request
|
|||
|
|
for ep in ['/test-ping', '/test-error', '/version', '/api/debug/ping', '/api/debug/error', '/api/debug/version', '/openapi.json']:
|
|||
|
|
try:
|
|||
|
|
r = urllib.request.urlopen(f'http://backend:8000{ep}', timeout=5)
|
|||
|
|
print(f'{ep:20s} {r.status} {r.read()[:100]}')
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f'{ep:20s} {getattr(e, \"code\", \"ERR\")} {str(e)[:80]}')"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
预期输出:6 个调试端点全部 4xx(404/403),/openapi.json 仍 404(P0-NEW11)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. 教训沉淀
|
|||
|
|
|
|||
|
|
| 教训 | 说明 | 适用范围 |
|
|||
|
|
|------|------|---------|
|
|||
|
|
| **L1: 端点挂载必须有显式门控** | 调试/测试端点不能与业务端点同等挂载,至少用 `if _is_dev_mode():` 包裹 | 所有 FastAPI / Flask 项目的 main entry |
|
|||
|
|
| **L2: 双重门控 = 单一真源 + 端点内自校验** | 防挂载逻辑异常 / 中间件异常导致的 fail-open | 任何安全敏感端点 |
|
|||
|
|
| **L3: 单一真源不写三份** | `_is_dev_mode()` 已有 main.py:48 实现,debug.py 委托调用而非重写 | 跨文件函数定义原则 |
|
|||
|
|
| **L4: 端点模块化便于审计** | 把同类端点集中到独立 router 文件,比散落在 main.py 更易发现遗漏 | 调试 / 监控 / 内部工具等非业务端点 |
|
|||
|
|
| **L5: 公网复测必须用直连而非走 nginx** | jumpserver-V2 容器 urllib 直测 backend:8000 是金标准,绕开 WAF 缓存 | 所有安全审计场景 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. 关联事项
|
|||
|
|
|
|||
|
|
### 7.1 P0-NEW11 → P0-NEW12 演进
|
|||
|
|
|
|||
|
|
| 序列 | 缺陷 | 修复 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| P0-NEW11 | `/openapi.json` 公网 200 暴露 API 字典 | docs_url/redoc_url/openapi_url 按 env 切 |
|
|||
|
|
| P0-NEW12 | 调试端点(test-ping/test-error/version)生产无门控 | 集中到 debug.py + 双重门控 |
|
|||
|
|
|
|||
|
|
**沿革**:P0-NEW11 修的是"自动文档",P0-NEW12 修的是"手写调试端点"。两者都属于"开发态工具暴露到生产"的同一类问题,未来扩展新调试端点都应走 debug.py。
|
|||
|
|
|
|||
|
|
### 7.2 类似遗漏检查(预防性审计)
|
|||
|
|
|
|||
|
|
| 文件 | 检查 | 状态 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `app/main.py` | 是否还有无门控的 `@app.get` 端点 | ✅ 修复后无 |
|
|||
|
|
| `app/api/dev_auth.py` | dev 端点门控 | ✅ 已有 `_dev_mode_enabled()` 二次校验 |
|
|||
|
|
| `app/api/auth_qrcode.py` | 是否有 debug 端点混入 | ✅ 无 |
|
|||
|
|
| `app/api/admin_*.py` | 管理端点是否过度暴露 | ✅ 需 AdminIPWhitelistMiddleware 兜底 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. 上线检查清单
|
|||
|
|
|
|||
|
|
- [x] main.py 语法 OK
|
|||
|
|
- [x] debug.py 语法 OK
|
|||
|
|
- [x] success_response import 清理
|
|||
|
|
- [x] dev_auth_router 仍正常挂载(不破坏现有)
|
|||
|
|
- [x] 后端镜像重建(jumpserver-V2 部署) ✅ 10:48 部署 v1 + 10:55 部署 v2
|
|||
|
|
- [x] 公网复测:6 个调试端点全部 4xx(10:55 实测 12/12 ✅)
|
|||
|
|
- [x] dev 模式自测:3 个新端点 200 正常(本地 10 用例全过)
|
|||
|
|
- [x] /health /ready /metrics 仍正常(运维端点)
|
|||
|
|
- [x] 容器内直测 6/6 全 404(v2 部署后)
|
|||
|
|
- [ ] git commit 推送 Gitea
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. 变更记录
|
|||
|
|
|
|||
|
|
| 日期 | 版本 | 变更内容 | 变更人 |
|
|||
|
|
|------|------|----------|--------|
|
|||
|
|
| 2026-08-12 | v1.0 | 缺陷单创建 + 修复方案制定 | Duckula |
|
|||
|
|
| 2026-08-12 | v1.0 | debug.py 新建 + main.py 三处端点删除 + dev 门控挂载 + 静态检查 OK | Duckula |
|
|||
|
|
| 2026-08-12 | v1.1 | **二次修复**:app_env 优先(生产永远 False) + 同步 dev_auth.py + conftest.py + 容器内 6/6 404 + 公网 12/12 4xx | Duckula |
|