Files
blackbird/docs/security/dnfs_SECURITY_AUDIT_20260801.md
T

11 KiB
Raw Blame History

dnfs 模块安全审计报告(Python 版)

原始审计日期: 2026-08-01
代码实证复核(最后更新): 2026-08-10
审计范围: dnfs/dnfs.py(723 行)、security_middleware.py、log_client.py
代码版本: dev_qing 分支
历史评级: B(2026-08-01 审计)


⚠️ 废弃声明

Python dnfs 已废弃,生产环境实际运行的是 C++20 重写版 dnfs2(见 dnfs2_SECURITY_AUDIT_20260808.md,评级 A)。core/init.txt 第 3 行启动的是 dnfs2/_run.sh,Python dnfs 不在运行清单内。

本审计仅覆盖 Python dnfs 的历史代码状态,用于溯源与和 dnfs2 的修复对照。不要在线上启用 Python dnfs——它的多数 P0/P1 安全问题未修复(见下方清单),且这些问题在 dnfs2 中已全部解决。


1. 模块定位与状态

dnfs = Dendrachy Network File-System,基于父子关系(pid)组织的树状节点服务,监听 127.0.0.1:8006,提供节点的增删改查,写操作需 gateway 签发的 Token。

  • Python dnfs(本审计对象):Flask + Waitress,历史实现,已废弃。
  • C++ dnfs2(生产实现):libhttp + libmsg + boost::json,接口兼容,安全基线显著提升(评级 A)。

接口(4 个 POST):get_notes(无鉴权)、add_note、delete_note、update_note(后三者需 Bearer Token;add/update 写入前经 ai_review 内容审核)。

2. 架构与数据流

客户端 → waf:443 → ca:8443(认证) → gateway:8445(纯转发, loopback)
   → dnfs2:8006 ──pymsg──► db2:8005 (execute 路由, 落 SQLite Notes 表)
            └──HTTP──────► ai_review:8007 (add/update 写入前内容审核)
  • Token 验签:dnfs 不持有密钥,统一转发 gateway /whoami 由 gateway 完成 RSA 验签 + 过期校验;gateway 不可达 → fail-closed 返回 503。
  • 数据操作:经 pymsg RPC 调用 db 服务 _route: "execute",不直接连库(规避 SQL 注入)。
  • 特殊节点(管理员专属):content 为 {"_sp":"link|js|image|file",...} 信封时前端差异化渲染;仅 is_admin 可发布。

3. 审计结论与评级

等级 原始审计(2026-08-03 验证) 当前代码实证(2026-08-10)
P0 1 项,已修复 1 2 项:部分修复 1,未修复 1*
P1 2 项,已修复 2 5 项:未修复 5
P2 2 项,未修复 2 5 项:全部未修复

* P0-4(父节点删除 TOCTOU)在 Python dnfs 中仍未修复,但其"未先检查 success"的子问题在 dnfs2 中已修,仅残余 TOCTOU 窗口(需 db2 事务支持)。

评级说明:Python dnfs 历史评级为 B(基于 2026-08-01 仅 5 项发现的快照)。按当前代码实证的全量 16 项发现,若仍上线则应下调评级——其 P0-4(树结构完整性)、P1-6/7/8/9/11(请求体、配置一致性、父节点校验、启动等待、类型转换)等高危问题均未修复。正确做法是不再启用 Python dnfs,统一使用 dnfs2。

4. 完整发现清单(代码实证,2026-08-10)

编号沿用 dnfs/README.md「已知问题与风险」的权威枚举(P0-14 / P1-511 / P2-12~16,共 16 项)。 状态依据对 dnfs/dnfs.py 与 log_client.py 的逐行核对。 已合入代码注释(dnfs.py)并移出本节的项:P0-1、P0-3、P1-5、P1-10(均为 ✅ 已修复/已解决),其安全决策见对应代码注释。本节仅保留未修复/部分修复的开放项,共 12 项。

🔴 P0 — 严重问题

编号 问题 状态 代码位置 说明
P0-2 review_content() fail-closed 与返回类型不一致 ⚠️ 基本修复 dnfs.py:156-202 异常路径(HTTPError/URLError/JSONDecodeError)均拒绝请求;但 HTTPError 且响应体为合法 JSON 时返回 dict(L196 return json.loads(error_body)),与成功路径返回的 bool 类型不一致,存在 True != dict → 一律拦截(仍 fail-closed,但类型契约不干净,建议清理)
P0-4 有子节点的父节点可能被删除(树结构完整性) ❌ 未修复 dnfs.py:510-525 + db/create_sqlite3.sql:13-19 应用层有子节点检查(if len(notes['data']) > 0: return 400),但:① 数据库层 Notes.pid 无外键约束;② 子节点查询与删除间存在 TOCTOU 竞态;③ notes['data'] 在 未先确认 success 时访问(L519),success=False 时抛 KeyError

🟠 P1 — 高危问题

编号 问题 状态 代码位置 说明
P1-6 请求体大小无限制 ❌ 未修复 dnfs.py:723 serve(app, host=_ADDR, port=_PORT) 未配置 max_request_body_size,建议与 WAF 对齐设 10MB
P1-7 log_client.py 配置与主模块不一致 ❌ 未修复 log_client.py:32-33 日志客户端直读 DB_HOST/DB_PORT 环境变量,不拉取 confs,与主模块配置源不一致(dnfs2 已复用主配置修复)
P1-8 update_note 改 pid 时未校验目标父节点存在 ❌ 未修复 dnfs.py:571-673 update_note 直接更新 pid,当 pid≠0 时不检查父节点是否存在(dnfs2 已用 _note_exists 校验)
P1-9 db RPC 客户端未启用启动等待 ❌ 未修复 dnfs.py:133 _rpc = RpcClient(_DBV2_ADDR, _DBV2_PORT, "dnfs") 无 retry_on_refused=True;db 未就绪时相关 API 直接失败(dnfs2 已加 retry_on_refused=true)
P1-11 note_id 缺少强制类型转换 ❌ 未修复 dnfs.py:474,577 仅类型提示 note_id: int = data.get('id') / id: int = data.get('id'),依赖 SQLite 类型亲和性隐式匹配(dnfs2 已用 get_int() + is_int64() 强校验)

🟡 P2 — 中危问题

编号 问题 状态 代码位置 说明
P2-12 错误响应泄露内部信息 ❌ 未修复 dnfs.py:371,395,400,420,523,540,561,615 错误消息拼接 request.remote_addr 与用户名返回客户端(如 未授权访问,从{ip}、特殊节点仅管理员可发布,用户{user},从{ip});应仅记服务端日志(dnfs2 已清除此类泄露)
P2-13 CORS 配置过于宽松 ❌ 未修复 dnfs.py:309 CORS(app) 默认允许所有来源(*),建议限制为实际前端域名
P2-14 _call_dbv2 返回值访问前未统一检查 success ❌ 未修复 dnfs.py:387,444,519,537 多处直接访问 notes['data'] 而不先确认 success==True,db 业务错误时可能 KeyError
P2-15 pid 参数判断逻辑不严谨 ❌ 未修复 dnfs.py:432 if not pid: pid = 0 将空字符串/None/0 等非法输入默默转为 0,掩盖参数错误
P2-16 遗留注释代码 ❌ 未修复 dnfs.py:17,491-507,636-648,677-711,715-718 大量注释掉的旧版 SQLAlchemy 直连逻辑与已废弃接口,影响可读性

5. 历史审计记录(2026-08-01 / 2026-08-03,作溯源)

原始审计仅覆盖 5 项发现,其验证结论如下(与第 4 节对应编号映射):

原编号 原问题 状态 映射至本章
P0-1 Token 无 RSA 签名验证 ✅ 已修复 已合入代码注释 (dnfs.py)
P1-1 Token 过期校验不严谨 ✅ 已修复 已合入代码注释 (dnfs.py)
P1-2 is_admin 权限完全依赖 Token 字段 ✅ 已修复 已并入 P0-1(gateway 服务端基于 Admin 白名单判定)
P2-1 AI 审核 fail-closed 实现需验证 ❌ 未修复(类型不一致) P0-2
P2-2 节点删除无确认机制 / 无速率限制 ❌ 未修复 P0-4(及一般加固项)

原始审计的「新增安全机制」表中以下项经代码实证仍然成立:

  • Token 验签委托 gateway(dnfs.py:312-351);fail-closed 503(dnfs.py:336-339)
  • 配置白名单合并(dnfs.py:55-63);配置完整性校验(dnfs.py:100-112)
  • 特殊节点审核脱敏(dnfs.py:273-286)
  • 统一异常处理 _handle_exp(dnfs.py:294-305)
  • 异步日志 fail-safe(log_client.py:66-72,147-148)

6. db 错误处理一致性矩阵(Python dnfs)

Python dnfs 多处直接访问 notes['data'] 而未先确认 success,与 dnfs2 的「db 业务错误统一返回 500」形成对比:

位置 查询目的 行为 风险
add_note 父节点检查 L387 notes['data'] 未查 success success=False 时 KeyError
get_notes L444 notes['data'] 未查 success success=False 时 KeyError
delete_note 子节点检查 L519 notes['data'] 未查 success success=False 时 KeyError
delete_note 所有权检查 L537 notes['data'] 未查 success success=False 时 KeyError

dnfs2 已将全部 select 的 db 业务错误统一返回 500(见 dnfs2_SECURITY_AUDIT_20260808.md §6 矩阵),此项在 dnfs2 已闭合。

7. 与 dnfs2 的修复对照

本章编号 Python dnfs 状态 dnfs2 状态
P0-2 ⚠️ 基本修复 ✅ 已修复(返回类型干净)
P0-4 ❌ 未修复 ⚠️ success 检查已修,TOCTOU 残余(L-2)
P1-6 ❌ 未修复 ✅ 已修复(setMaxBodySize(10MB))
P1-7 ❌ 未修复 ✅ 已修复(日志复用主配置)
P1-8 ❌ 未修复 ✅ 已修复(pid≠0 校验父节点)
P1-9 ❌ 未修复 ✅ 已修复(retry_on_refused=true)
P1-11 ❌ 未修复 ✅ 已修复(get_int() 强类型)
P2-12 ❌ 未修复 ✅ 已修复(清除 IP/用户名泄露)
P2-13 ❌ 未修复 记录(前端架构限制 L-7)
P2-14 ❌ 未修复 ✅ 已修复(_call_db 统一查 success)
P2-15 ❌ 未修复 ✅ 已修复(严格类型 + 负数校验)
P2-16 ❌ 未修复 ✅ 已清理

结论:dnfs2 基本闭合了 Python dnfs 的全部 P0/P1 及大部分 P2;Python dnfs 的未修复项无需在 Python 版继续投入,统一以 dnfs2 为准。

8. 修复路线图(仅针对需要继续维护 Python dnfs 的极端场景)

立即(如被迫启用 Python dnfs)

  • P0-4:Notes 表 pid 加 FOREIGN KEY ... REFERENCES Notes(id) ON DELETE RESTRICT;应用层先查 success 再访问 data
  • P1-6:serve() 配置 max_request_body_size=10MB
  • P2-14:封装统一的 db 调用检查 success

短期

  • P1-7:日志客户端复用 confs 主配置
  • P1-8:update_note 改 pid 前校验父节点存在
  • P1-9:RpcClient 启用 retry_on_refused=True
  • P1-11:note_id 显式 int() 转换 + 校验
  • P2-12:错误消息去除 IP/用户名,仅记日志

中期

  • P2-13:CORS 限制为实际前端域名
  • P2-15:pid 参数严格校验(拒绝空串/None)
  • P2-16:清理遗留注释代码
  • P0-2:清理 review_content 返回类型不一致

推荐替代方案:直接弃用 Python dnfs,全部流量走 dnfs2(已在 core/init.txt 生效)。