Files
wecom_it_smart_desk/docs/03-测试文档/05-缺陷单/BUG-安全-005-调试端点无门控-001.md
T
Simon 0042519f66 fix(security): P0-NEW12 调试端点代码层根因(debug.py 集中 + app_env 优先)
【触发】
2026-08-12 09:00 早班巡检:jumpserver-V2 容器内 `python urllib` 绕开 nginx 直测
FastAPI(backend:8000):
  - /test-ping 200 pong
  - /test-error 200 "服务器内部错误"
  - /version 200 1.1.0(含 git hash)
  - /openapi.json 404(P0-NEW11 已闭环)
真实风险:公网 404 全部依赖 nginx 边缘层兜底;若 nginx 配置被误改 /
失效 / 容器间网络可达 backend:8000,攻击者可拿到 3 个调试端点。

【修复(v1.0 + v1.1 二次修复)】
1. 新建 src/backend/app/api/debug.py 集中 3 个调试端点(/api/debug/ping
   /error /version),加双重门控:
   - 模块级:main.py `if _is_dev_mode():` 块内 include_router(debug_router)
   - 端点级:每个端点内部 _is_dev_mode() 二次校验(防 fail-open)
2. 删除 main.py 散落的 /test-ping /test-error /version
3. debug.py 的 _is_dev_mode() 委托 main.py:48 单一真源(避免三处定义漂移)
4. 【v1.1 二次修复】_is_dev_mode() / _dev_mode_enabled() 改为 app_env 优先:
   - 优先级 1: app_env=="production" → 永远 False(即使 DEV_MODE=true)
   - 优先级 2: 非 production → 看 DEV_MODE / settings.dev_mode
   同步修复 main.py + dev_auth.py(两个独立实现不能漏一处)
5. tests/conftest.py enable_dev_mode fixture 同步设 APP_ENV=development
   (避免 dev 测试 case 因 app_env 默认 production 而 fail)

【验证(公网 + 容器内)】
- 公网 12 端点:3 旧端点 404 + 3 新端点 404 + 3 dev 403(nginx IP 白名单)+
  1 health 200 + 3 P0-NEW11 404 
- 容器内直连 backend:8000 6 端点:100% 4xx (用户原话真实风险场景)
- 回归测试 10 passed + 1 skipped(psutil 缺包)

【部署】
jumpserver-V2 PSFTP + sudo cp + chown admin:admin + 清理 __pycache__ +
docker restart wecom_it_backend
  v1 部署:10:48
  v2 部署:10:55(app_env 优先修复后)

【关联】
- 缺陷单:docs/03-测试文档/05-缺陷单/BUG-安全-005-调试端点无门控-001.md
- 看板:项目状态看板 v1.9.7-DRAFT P0-NEW12  已修复
- 工作日志:.workbuddy/memory/2026-08-12.md
- MEMORY 铁律:.workbuddy/memory/MEMORY.md「调试端点铁律」5 条
2026-08-12 11:01:14 +08:00

299 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 缺陷单:调试端点(/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
> **指派人**: 宋献
> **修复人**: DuckulaAI 助手)
> **修复日期**: 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.cn10.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 404P0-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 挂载逻辑有 bugdebug.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 个调试端点全部 4xx404/403),/openapi.json 仍 404P0-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 |