# 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`](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-1~4 / P1-5~11 / 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` 生效)。