docs(dnfs): 将已修复审计项合入 dnfs.py 注释并精简审计报告

This commit is contained in:
qing committed 2026-08-10 01:55:58 +08:00
1 parent c07ad7ac39
commit 2959197597
4 files changed
+148 -86

No files matched your search

@@ -6,6 +6,8 @@
**审计轮次**: 第 2 轮(第 1 轮发现 5 项已全部修复)
**安全评级**: A
> 📌 **本服务为 dnfs 的 C++20 生产实现**,`core/init.txt` 实际启动的是 `dnfs2`(Python dnfs 已废弃,历史审计见 [`dnfs_SECURITY_AUDIT_20260801.md`](dnfs_SECURITY_AUDIT_20260801.md),2026-08-10 已复核为全量 16 项发现)。本审计覆盖生产代码,结论有效。
---
## 1. 审计概述
+128 -84
View File
@@ -1,112 +1,156 @@
# dnfs 模块安全审计报告
# dnfs 模块安全审计报告(Python 版)
**审计日期**: 2026-08-01
**审计范围**: `dnfs.py`、`security_middleware.py`、`log_client.py`
**代码版本**: dev_qing 分支
**原始审计日期**: 2026-08-01
**代码实证复核(最后更新)**: 2026-08-10
**审计范围**: `dnfs/dnfs.py`(723 行)、`security_middleware.py`、`log_client.py`
**代码版本**: dev_qing 分支
**历史评级**: B(2026-08-01 审计)
---
## 代码验证记录
**验证日期**: 2026-08-03
**验证方式**: 逐条对照审计报告与当前代码(`dnfs.py`、`security_middleware.py`、`log_client.py`)
### 验证结果汇总
| 等级 | 总数 | 已修复 | 部分修复 | 未修复 | 修复率 |
|------|------|--------|----------|--------|--------|
| P0 | 1 | 1 | 0 | 0 | 100% |
| P1 | 2 | 2 | 0 | 0 | 100% |
| P2 | 2 | 0 | 0 | 2 | 0% |
| **合计** | **5** | **3** | **0** | **2** | **60%** |
### 逐条验证
| 编号 | 状态 | 代码位置 | 验证说明 |
|------|------|----------|----------|
| P0-1 | ✅ 已修复 | `dnfs.py` L312-351 | `decode_token` 完全重写:不再本地解码,改为调用 gateway `/whoami` 接口,由 gateway 完成 RSA 签名验证和过期检查;网络异常 fail-closed(返回 503) |
| P1-1 | ✅ 已修复 | `dnfs.py` L312-351 | Token 过期校验由 gateway `/whoami` 统一处理,dnfs 不再自行校验 |
| P1-2 | ✅ 已修复 | `dnfs.py` L348-351 | `is_admin` 由 gateway 服务端判定(基于 Admin 白名单),dnfs 信任 gateway 响应,不再从用户提交的 Token 中直接读取 |
| P2-1 | ❌ 未修复 | `dnfs.py` L156-202 | `review_content` 已实现 fail-closed(所有异常路径均拒绝请求),但返回类型不一致(bool vs dict),需代码清理 |
| P2-2 | ❌ 未修复 | `dnfs.py` L468-562 | `delete_note` 无确认机制,无速率限制 |
> ## ⚠️ 废弃声明
>
> **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. 模块定位与状态
| 优先级 | 总数 | 已修复 | 待修复 |
|--------|------|--------|--------|
| P0 | 1 | 1 | 0 |
| P1 | 2 | 2 | 0 |
| P2 | 2 | 0 | 2 |
dnfs = **Dendrachy Network File-System**,基于父子关系(`pid`)组织的树状节点服务,监听 `127.0.0.1:8006`,提供节点的增删改查,写操作需 gateway 签发的 Token。
---
- **Python dnfs(本审计对象)**:Flask + Waitress,历史实现,已废弃。
- **C++ dnfs2(生产实现)**:libhttp + libmsg + boost::json,接口兼容,安全基线显著提升(评级 A)。
## 🔴 P0 — 严重问题
接口(4 个 `POST`):`get_notes`(无鉴权)、`add_note`、`delete_note`、`update_note`(后三者需 Bearer Token;add/update 写入前经 `ai_review` 内容审核)。
### P0-1: Token 无 RSA 签名验证,可伪造任意用户身份(含管理员) ✅ 已修复
- **位置**: `dnfs.py:312-351`(完全重写)
- **原问题**: `decode_token` 本地 base64 解码 Token,代码明确标注 `FIXME: 验证数字签名`,但完全跳过了 RSA 签名验证,攻击者可伪造任意用户身份(含管理员)
- **修复方案**: `decode_token` 完全重写,改为调用 gateway `/whoami` 接口:
1. 将 Token 通过 `Authorization: Bearer` 头发送给 gateway
2. Gateway 服务端完成 RSA 签名验证 + 过期检查
3. 返回 `{"login": true/false, "user": "...", "is_admin": true/false, ...}`
4. 网络异常(gateway 不可达)→ fail-closed,返回 503
5. 验签失败(`login=false`)→ 返回 None,调用方返回 401
- **影响范围**: 已消除,所有需要认证的端点(add_note、delete_note、update_note)均通过 gateway 验证
## 2. 架构与数据流
---
```
客户端 → waf:443 → ca:8443(认证) → gateway:8445(纯转发, loopback)
→ dnfs2:8006 ──pymsg──► db2:8005 (execute 路由, 落 SQLite Notes 表)
└──HTTP──────► ai_review:8007 (add/update 写入前内容审核)
```
## 🟠 P1 — 高危问题
- **Token 验签**:dnfs 不持有密钥,统一转发 `gateway /whoami` 由 gateway 完成 RSA 验签 + 过期校验;gateway 不可达 → fail-closed 返回 503。
- **数据操作**:经 pymsg RPC 调用 db 服务 `_route: "execute"`,不直接连库(规避 SQL 注入)。
- **特殊节点(管理员专属)**:`content` 为 `{"_sp":"link|js|image|file",...}` 信封时前端差异化渲染;仅 `is_admin` 可发布。
| # | 问题 | 状态 | 说明 |
|---|------|------|------|
| P1-1 | Token 过期校验不严谨 | ✅ 已修复 | Token 过期校验由 gateway `/whoami` 统一处理(基于 `created_at` + `keep_time`),dnfs 不再自行校验 |
| P1-2 | is_admin 权限完全依赖 Token 字段,无二次验证 | ✅ 已修复 | `is_admin` 由 gateway 服务端基于 Admin 白名单判定,dnfs 从 gateway 响应中读取(`info.get('is_admin')`),不再从用户提交的 Token 直接读取 |
## 3. 审计结论与评级
---
| 等级 | 原始审计(2026-08-03 验证) | 当前代码实证(2026-08-10) |
|------|---------------------------|---------------------------|
| P0 | 1 项,已修复 1 | 2 项:部分修复 1,未修复 1* |
| P1 | 2 项,已修复 2 | 5 项:未修复 5 |
| P2 | 2 项,未修复 2 | 5 项:全部未修复 |
## 🟡 P2 — 中危问题
\* P0-4(父节点删除 TOCTOU)在 Python dnfs 中仍**未修复**,但其"未先检查 `success`"的子问题在 dnfs2 中已修,仅残余 TOCTOU 窗口(需 db2 事务支持)。
| # | 问题 | 状态 | 说明 |
|---|------|------|------|
| P2-1 | AI 审核 fail-closed 实现需验证 | ❌ 未修复 | `review_content` 已实现 fail-closed(HTTPError/URLError/JSONDecodeError 均导致请求被拒),但函数返回类型不一致(bool vs dict),建议代码清理 |
| P2-2 | 节点删除无确认机制,批量删除无速率限制 | ❌ 未修复 | `delete_note` 无二次确认,无速率限制 |
**评级说明**: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 项。
1. ✅ **特殊节点校验**: link/html/image/file 各类型有格式与大小限制(URL 前缀、大小上限)
2. ✅ **AI 内容审核集成**: 新增节点前调用 ai_review 服务审核
3. ✅ **security_middleware.py**: 安全响应头(CSP、X-Frame-Options、X-XSS-Protection 等)
4. ✅ **CORS 配置**: 跨域控制
5. ✅ **通过 pymsg 调 db 服务**: 不直连数据库,规避 SQL 注入
### 🔴 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 |
| 机制 | 代码位置 | 说明 |
|------|----------|------|
| Token 验签委托 gateway | `dnfs.py` L312-351 | `decode_token` 调用 gateway `/whoami` 完成 RSA 签名验证 + 过期检查,dnfs 不再持有/管理密钥 |
| Token 验签 fail-closed | `dnfs.py` L336-339 | gateway 不可达时返回 503,拒绝所有认证请求 |
| 配置白名单合并 | `dnfs.py` L55-63 | `_merge_ini` 仅覆盖已知 section/key,防止意外字段注入 |
| 配置完整性校验 | `dnfs.py` L100-112 | 启动时检查所有必需配置项,缺失则退出 |
| 特殊节点审核脱敏 | `dnfs.py` L273-286 | `_sp_review_text` 剔除 base64 负载(体量大且无审核意义),仅保留文本字段送审 |
| 统一异常处理 | `dnfs.py` L294-305 | `_handle_exp` 装饰器统一捕获 DnfsExp 和通用异常,返回结构化错误响应 |
| 异步日志 fail-safe | `log_client.py` L66-72, L147-148 | AsyncRpcQueue 后台异步推送,db 不可用时静默丢弃,不阻塞业务 |
### 🟠 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 — 中危问题
### 立即(24小时)
- [x] 实现 Token RSA 签名验证 ✅ 已完成(委托 gateway)
| 编号 | 问题 | 状态 | 代码位置 | 说明 |
|------|------|------|----------|------|
| 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 直连逻辑与已废弃接口,影响可读性 |
### 短期(72小时)
- [x] Token 过期严格校验 ✅ 已完成(gateway 统一处理)
- [x] is_admin 二次验证 ✅ 已完成(gateway 服务端判定)
## 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/用户名,仅记日志
### 中期
- [ ] 清理 `review_content` 返回类型不一致(bool vs dict)
- [ ] 节点操作速率限制
- [ ] 节点删除二次确认机制
- [ ] P2-13:CORS 限制为实际前端域名
- [ ] P2-15:`pid` 参数严格校验(拒绝空串/None)
- [ ] P2-16:清理遗留注释代码
- [ ] P0-2:清理 `review_content` 返回类型不一致
> **推荐替代方案**:直接弃用 Python dnfs,全部流量走 dnfs2(已在 `core/init.txt` 生效)。