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

12 KiB
Raw Blame History

缺陷单:调试端点(/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.pyline 975-983 旧 /test-ping/test-error、line 1052-1076 旧 /version
新增: src/backend/app/api/debug.py(集中管理)
触发条件 1. 容器间网络可达 backend:8000(如 docker network wecom_it_default 内其他容器,nginx 失效时的公网直连)
2. 攻击者通过 python urllib 绕开 nginx 直接访问 backend:8000
3. /test-ping 200 pong(暴露后端存活)/test-error 200 "服务器内部错误"(暴露异常处理路径)/version 200 {version:"1.1.0"}(暴露 git hash 可锁定已知漏洞)

2. 漏洞证据

2.1 漏洞扫描结果(2026-08-12 jumpserver-V2 容器内直测)

# 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
(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,核心结构:

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):

# 删除
@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 旁边):

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)
    # ...

清理无用 importsuccess_response 不再使用):

# 改前
from app.utils.response import AppException, app_exception_handler, success_response
# 改后
from app.utils.response import AppException, app_exception_handler

5. 验证

5.1 静态检查

$ 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,扫描路由表:

# 预期(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 未设)

# 期望全部 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

# 期望全部 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

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. 上线检查清单

  • main.py 语法 OK
  • debug.py 语法 OK
  • success_response import 清理
  • dev_auth_router 仍正常挂载(不破坏现有)
  • 后端镜像重建(jumpserver-V2 部署) 10:48 部署 v1 + 10:55 部署 v2
  • 公网复测:6 个调试端点全部 4xx(10:55 实测 12/12
  • dev 模式自测:3 个新端点 200 正常(本地 10 用例全过)
  • /health /ready /metrics 仍正常(运维端点)
  • 容器内直测 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