docs(dnfs): update README with security audit findings and architecture corrections
- Fix architecture diagram to reflect pymsg TCP RPC (not HTTP), add confs dependency - Update security headers to match actual security_middleware.py values - Add complete P0/P1/P2 security audit findings (16 issues total): - P0: Token without RSA signature verification (forgery risk) - P0: review_content() fail-closed broken due to exception handling gaps - P0: delete_note/update_note missing @_handle_exp decorator - P0: Parent nodes with children can be deleted (no DB foreign key + TOCTOU race) - P1: No token expiry check, no request body size limit, log_client config mismatch - P2: Error message info leakage, overly permissive CORS, legacy commented code - Correct file paths for frontend (static/public/), dependencies (pymsg, not nuitka) - Fix outdated code line references
This commit is contained in:
1 parent
7ef6a0b65f
commit
b30ce1424b
1 file changed
+65
-21
+65
-21
@@ -14,13 +14,16 @@ Dendrachy Network File-System,基于父子关系(pid)组织的树状节点
|
|||||||
▼
|
▼
|
||||||
dnfs :8006 (Flask + Waitress)
|
dnfs :8006 (Flask + Waitress)
|
||||||
│
|
│
|
||||||
├──► db :8005 数据持久化(SQL-over-HTTP,/execute)
|
├──► confs :8010 启动时拉取配置(dnfs.ini,强依赖,retry_on_refused 等待就绪)
|
||||||
└──► ai_review :8007 内容审核(仅 add_note / update_note 时调用)
|
├──► db :8005 数据持久化(pymsg TCP RPC,execute 路由)
|
||||||
|
└──► ai_review :8007 内容审核(HTTP POST /review,仅 add_note/update_note 时调用)
|
||||||
```
|
```
|
||||||
|
|
||||||
- 不直连数据库,所有数据操作通过 HTTP 调用 `db` 服务(`http://127.0.0.1:8005/execute`)。
|
- 启动时通过 pymsg 从 confs 拉取 `dnfs.ini` 配置,拉取失败直接退出(强依赖);使用 `retry_on_refused=True` 自动等待 confs 就绪。
|
||||||
- 新增/更新节点前调用 `ai_review` 服务(`http://127.0.0.1:8007/review`)进行内容审核,审核不通过则拒绝写入。
|
- 不直连数据库,所有数据操作通过 pymsg TCP RPC 调用 `db` 服务(`_route: "execute"`),规避 SQL 注入风险。
|
||||||
- 通常部署在 gateway 之后(`Proxy.opened` 白名单中),由 gateway 完成 Token 验签后转发,dnfs 仅做所有权校验。
|
- 新增/更新节点前调用 `ai_review` 服务进行内容审核(HTTP POST `{content: "..."}`),审核不通过则拒绝写入。
|
||||||
|
- 日志通过 pymsg `AsyncRpcQueue` 异步批量写入 db 的 `Logs` 表,fail-safe 不阻塞业务。
|
||||||
|
- 通常部署在 gateway/WAF 之后(`Proxy.opened` 白名单中),由 gateway 完成 Token RSA 验签后转发。
|
||||||
|
|
||||||
## 数据模型
|
## 数据模型
|
||||||
|
|
||||||
@@ -42,7 +45,7 @@ Dendrachy Network File-System,基于父子关系(pid)组织的树状节点
|
|||||||
Authorization: Bearer <base64_payload>_<rsa_sign>
|
Authorization: Bearer <base64_payload>_<rsa_sign>
|
||||||
```
|
```
|
||||||
|
|
||||||
- Token 由 gateway 在 `/login` 签发,dnfs 通过 [dnfs.py:163](dnfs.py#L163) `decode_token` 仅解 Base64 取 payload,**不验签**(验签由 gateway 完成)。
|
- Token 由 gateway 在 `/login` 签发,dnfs 通过 [dnfs.py:304](dnfs.py#L304) `decode_token` 仅解 Base64 取 payload,**当前代码不验签**(代码中标注 `# FIXME: 验证数字签名`,依赖部署上经 gateway 转发的网络隔离)。
|
||||||
- payload 中取 `user` 字段作为操作者,写入 `owner`。
|
- payload 中取 `user` 字段作为操作者,写入 `owner`。
|
||||||
- `delete_note` 与 `update_note` 校验所有权:
|
- `delete_note` 与 `update_note` 校验所有权:
|
||||||
- 节点 `owner` 必须等于当前 `user`,否则 `403`;
|
- 节点 `owner` 必须等于当前 `user`,否则 `403`;
|
||||||
@@ -208,10 +211,13 @@ curl -X POST http://localhost:8006/update_note \
|
|||||||
|
|
||||||
## 安全特性
|
## 安全特性
|
||||||
|
|
||||||
- **安全响应头**([security_middleware.py](security_middleware.py)):`X-Frame-Options: DENY`、`X-Content-Type-Options: nosniff`、`Referrer-Policy`、`Permissions-Policy`、CSP(`default-src 'self'`、`frame-ancestors 'none'`),并移除 `Server` 头。
|
- **安全响应头**([security_middleware.py](security_middleware.py)):`X-Frame-Options: SAMEORIGIN`、`X-XSS-Protection`、`X-Content-Type-Options: nosniff`、`Referrer-Policy: strict-origin-when-cross-origin`、`Permissions-Policy`(禁用摄像头/麦克风/地理位置)、CSP(`default-src 'self'` 等),并移除 `Server` 头。
|
||||||
- **Token 鉴权**:写操作强制校验,payload 取 `user` 字段。
|
- **Token 鉴权**:写操作强制校验,payload 取 `user` 字段。
|
||||||
- **所有权隔离**:普通用户只能改/删自己的节点。
|
- **所有权隔离**:普通用户只能改/删自己的节点。
|
||||||
- **内容审核**:写入与更新前调用 `ai_review`,色情/违禁/赌博/诈骗/暴力等内容会被拒绝。
|
- **特殊节点管控**:link/html/image/file 类型有格式校验与大小限制(URL 前缀、文件名长度、内容大小上限)。
|
||||||
|
- **内容审核**:写入与更新前调用 `ai_review`,非法内容会被拒绝(fail-closed 设计,见已知问题)。
|
||||||
|
- **异步日志**:通过 pymsg `AsyncRpcQueue` 异步批量写日志,fail-safe 不阻塞业务线程。
|
||||||
|
- **非 root 容器化运行**:Dockerfile 中使用 appuser 非特权用户运行服务。
|
||||||
|
|
||||||
## 配置
|
## 配置
|
||||||
|
|
||||||
@@ -266,17 +272,17 @@ dnfs 通过 pymsg `retry_on_refused=True` 自动阻塞等待 confs 就绪,**
|
|||||||
|
|
||||||
## 前端
|
## 前端
|
||||||
|
|
||||||
三文件架构:
|
前端页面由 `static` 服务承载,不在 dnfs 模块内:
|
||||||
|
|
||||||
```
|
```
|
||||||
templates/index.html 页面结构(Font Awesome 图标)
|
static/public/dnfs.html 页面结构
|
||||||
static/css/style.css 样式
|
static/public/css/ 样式(index.css / aibox.css 等)
|
||||||
static/js/script.js 交互逻辑
|
static/public/js/dnfs.js 交互逻辑
|
||||||
```
|
```
|
||||||
|
|
||||||
功能:节点树浏览、新增/编辑/删除节点、文件 Base64 上传与下载、节点迁移(移动到其他父节点下)。
|
功能:节点树浏览、新增/编辑/删除节点、文件 Base64 上传与下载、节点迁移(移动到其他父节点下)。
|
||||||
|
|
||||||
> 注:首页路由 `@app.route('/')` 当前已注释,前端页面实际由 `static` 服务的 `dnfs.html` 承载。
|
> 注:首页路由 `@app.route('/')` 和 `render_template` 相关代码已注释,dnfs 仅提供 JSON API,不直接提供 HTML 页面。
|
||||||
|
|
||||||
## 依赖
|
## 依赖
|
||||||
|
|
||||||
@@ -284,12 +290,12 @@ static/js/script.js 交互逻辑
|
|||||||
flask
|
flask
|
||||||
flask-cors
|
flask-cors
|
||||||
waitress
|
waitress
|
||||||
nuitka # 可选,编译为独立二进制
|
pymsg # 内部 RPC 通信库(../pymsg/)
|
||||||
```
|
```
|
||||||
|
|
||||||
> `requirements.txt` 仍列有 `flask_sqlalchemy`,但代码已不使用(SQLAlchemy 直连代码已注释),属遗留依赖。
|
运行时需要 `confs`(:8010,启动强依赖)、`db`(:8005)和 `ai_review`(:8007)服务可达;对外访问需经 gateway 转发并验签。
|
||||||
|
|
||||||
运行时需要 `db`(:8005)和 `ai_review`(:8007)服务已启动;对外访问需经 gateway 转发并验签。
|
> `requirements.txt` 中曾列有 `flask_sqlalchemy`,Dockerfile 中已添加 `pip uninstall -y flask_sqlalchemy` 清理遗留依赖;代码中的 SQLAlchemy 直连逻辑已全部注释,通过 pymsg RPC 调用 db 服务。
|
||||||
|
|
||||||
## 运行
|
## 运行
|
||||||
|
|
||||||
@@ -300,8 +306,46 @@ bash _run.sh # 启动服务(waitress)
|
|||||||
|
|
||||||
## 已知问题与风险
|
## 已知问题与风险
|
||||||
|
|
||||||
- **`decode_token` 未验签**([dnfs.py:167](dnfs.py#L167)):仅解 Base64 取 payload,存在 `# FIXME: 验证数字签名` 标记。当前安全性依赖"必须经 gateway 转发"这一部署假设;若 dnfs 被直接访问(绕过 gateway),任意伪造的 Token 都能通过。建议补 RSA 验签或在部署上确保仅 gateway 可达。
|
审计日期: 2026-08-01
|
||||||
- **`review_content` 返回类型不一致**([dnfs.py:73-119](dnfs.py#L73)):正常路径返回 `bool`([L105](dnfs.py#L105)),HTTPError 路径返回 `dict`([L113](dnfs.py#L113))。调用处用 `True != review` 判断,HTTPError 时 `dict != True` 恒为真,会一律判为审核失败并吞掉具体错误信息。建议统一返回类型或在调用处 catch 异常。
|
|
||||||
- **CORS 全开**:`CORS(app)` 未配置白名单,允许任意 Origin 跨域访问。考虑到写操作需 Token,风险可控,但仍建议按 gateway 的 `[CORS] allowed_origins` 方式收紧。
|
### 🔴 P0 — 严重问题(需立即修复)
|
||||||
- **`call_db_service` 异常未捕获**:调用处([L208](dnfs.py#L208)、[L261](dnfs.py#L261)、[L318](dnfs.py#L318)、[L347](dnfs.py#L347)、[L440](dnfs.py#L440))直接取 `result['success']` / `notes['data']`,若 db 服务返回非预期结构或网络异常会抛 500。
|
|
||||||
- **`get_notes` 不鉴权**:任何能访问 dnfs 的人都能枚举所有节点内容。若内容含敏感 Base64 文件,需在 gateway 层做路径级鉴权。
|
1. **Token 无 RSA 签名验证,可伪造任意用户身份(含管理员)** — [dnfs.py:304-311](dnfs.py#L304-L311)
|
||||||
|
- `decode_token()` 直接 base64 解码 payload,完全跳过 RSA 签名验证(代码中有 `# FIXME: 验证数字签名` 注释)
|
||||||
|
- 若 dnfs 被直接访问(绕过 gateway),攻击者可构造 `{"user": "任意邮箱", "is_admin": true}` 的 payload,base64 编码后拼接下划线+任意字符串即可伪造管理员身份,绕过所有权限校验
|
||||||
|
- **参考实现**: gateway `verifyWsAdminToken` 展示了正确流程:分割 `_` → base64 解码 → RSA 公钥验签 → 检查过期时间
|
||||||
|
- **修复建议**: 实现 RSA 签名验证(持有 gateway 公钥)+ Token 过期校验(`iat`/`created_at` + `keep_time`)
|
||||||
|
|
||||||
|
2. **`review_content()` 错误处理断裂,fail-closed 失效** — [dnfs.py:149-195](dnfs.py#L149-L195)
|
||||||
|
- 成功路径返回 `bool`,但 HTTPError/URLError/其他异常直接 `raise e`,没有返回 `False` 实现 fail-closed
|
||||||
|
- 内层 `except json.JSONDecodeError as e` 变量名遮蔽外层 `HTTPError as e`
|
||||||
|
- ai_review 不可用时异常向上传播导致 500,而非拒绝内容(违反 fail-closed 设计意图)
|
||||||
|
- **修复建议**: 所有异常路径返回 `False` 或抛 DnfsExp 拒绝写入
|
||||||
|
|
||||||
|
3. **`delete_note` 和 `update_note` 缺少 `@_handle_exp` 装饰器** — [dnfs.py:428](dnfs.py#L428), [dnfs.py:531](dnfs.py#L531)
|
||||||
|
- `add_note`/`get_notes` 使用了 `@_handle_exp` 统一异常处理,但 `delete_note`/`update_note` 没有
|
||||||
|
- 抛出 `DnfsExp` 或其他异常时返回 Flask 默认 500 HTML 错误页,与其他接口 JSON 响应不一致
|
||||||
|
|
||||||
|
4. **有子节点的父节点可能被删除(树结构完整性保护缺陷)**
|
||||||
|
- **数据库层缺失外键约束**: 虽然 dbv2 启用了 `PRAGMA foreign_keys=ON`,但 Notes 表建表 SQL([db/create_sqlite3.sql:13-19](../db/create_sqlite3.sql#L13-L19))中 `pid` 列只是普通 INTEGER,没有定义 `FOREIGN KEY (pid) REFERENCES Notes(id) ON DELETE RESTRICT`
|
||||||
|
- **应用层 TOCTOU 竞态**: 子节点查询(SELECT)和删除(DELETE)是两个独立 RPC 调用,无事务保护,检查与删除之间可插入子节点导致父节点被删后子节点成为孤儿
|
||||||
|
- **错误处理缺陷**: `_call_dbv2` 返回 `success=False` 时响应中无 `data` 键,直接访问 `notes['data']` 会抛 KeyError 而非正确处理
|
||||||
|
- **修复建议**: (a) 添加数据库外键约束; (b) 应用层检查返回值 `success` 状态; (c) 使用数据库事务将检查+删除原子化
|
||||||
|
|
||||||
|
### 🟠 P1 — 高危问题
|
||||||
|
|
||||||
|
5. **Token 无过期校验** — payload 中包含 `iat`/`created_at`/`keep_time` 字段,但 `decode_token()` 完全不检查,泄露的 Token 可永久使用
|
||||||
|
6. **请求体大小无限制** — Waitress 未配置 `max_request_body_size`,可能被大请求体 DoS;建议设置 10MB 上限与 WAF 对齐
|
||||||
|
7. **`log_client.py` 配置与主模块不一致** — [log_client.py:32-33](log_client.py#L32-L33) 直接读 `DB_HOST`/`DB_PORT` 环境变量,不从 confs 拉取配置,可能连错地址
|
||||||
|
8. **`update_note` 修改 pid 时未验证目标父节点存在** — `add_note` 会检查 pid≠0 时父节点是否存在,但 `update_note` 直接更新不检查,可将节点移动到不存在的父节点下
|
||||||
|
9. **db RPC 客户端未启用启动等待** — `_rpc = RpcClient(...)` 未设置 `retry_on_refused=True`,db 未就绪时首次请求失败(虽然 confs 连接启用了重试)
|
||||||
|
10. **`delete_note`/`update_note` 中 `decode_token` 无 try-except** — `get_notes` 中包裹了异常处理,但这两个函数没有,base64 解码/JSON 解析错误直接抛 500
|
||||||
|
11. **`note_id` 缺少强制类型转换** — `note_id: int = data.get('id')` 只是类型提示,不会实际转换;传入字符串 `"5"` 依赖 SQLite 类型亲和性隐式匹配
|
||||||
|
|
||||||
|
### 🟡 P2 — 中危问题
|
||||||
|
|
||||||
|
12. **错误响应泄露内部信息** — 多处错误消息拼接 `request.remote_addr` 和用户名返回给客户端(如"未授权访问,从xxx"),应仅记录到服务端日志
|
||||||
|
13. **CORS 配置过于宽松** — `CORS(app)` 默认允许所有来源(`*`),建议限制为实际前端域名
|
||||||
|
14. **`_call_dbv2` 返回值访问前未统一检查 success** — 多处直接访问 `notes['data']` 而不先确认 `success==True`
|
||||||
|
15. **`pid` 参数判断逻辑不严谨** — `if not pid: pid = 0` 会将空字符串/None 等非法输入默默转为 0,掩盖参数错误
|
||||||
|
16. **遗留注释代码** — 文件中有大量注释掉的旧版 SQLAlchemy 代码和已废弃接口,影响可读性
|
||||||
Reference in new issue
Block a user