From 9b6aa39b94ac8af511b033139b1741b646ab7ffb Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Thu, 13 Aug 2026 04:10:35 +0800 Subject: [PATCH] =?UTF-8?q?fix(dnfs2):=20update=5Fnote=20=E6=88=90?= =?UTF-8?q?=E7=8E=AF=E6=A3=80=E6=B5=8B=EF=BC=88=E4=BF=AE=E5=A4=8D=E5=B7=B2?= =?UTF-8?q?=E7=9F=A5=E9=99=90=E5=88=B6=20L-1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 _is_descendant 祖先走查:改 pid 时从新父节点沿 pid 链逐级向上, 命中自身即拒绝(自引用/移入自身子树均成环,子树不可达);深度上限 64 + visited 集防存量脏数据死循环,db 错误 fail-closed 抛 500。 README 已知限制 L-1 移入安全修复表。 --- dnfs2/README.md | 2 +- dnfs2/src/dnfs2.cpp | 53 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/dnfs2/README.md b/dnfs2/README.md index b2d66e6..57e7149 100644 --- a/dnfs2/README.md +++ b/dnfs2/README.md @@ -71,10 +71,10 @@ dnfs2 :8006 ──pymsg──► db2 :8005 (execute 路由) ──► SQLite | P1-8 | update_note 改 pid 未校验父节点 | pid≠0 时校验父节点存在 | | P1-9 | db RPC 未启用启动等待 | `RpcClient(retry_on_refused=true)` | | P1-11 | note_id 无强制类型转换 | 显式 `as_int64()` + 类型校验 | +| L-1 | update_note 允许创建循环 | `_is_descendant` 祖先走查:拒绝新父节点为自身或位于自身子树内(深度上限 64,防存量环死循环) | ## 已知限制(P2,设计决策不修复) -- L-1: update_note 允许创建循环(需图遍历检测后代关系) - L-2: delete_note TOCTOU 残余(需 db2 事务支持) - L-4: 无速率限制(gateway/WAF 职责) - L-5: title/content 无最大长度校验 diff --git a/dnfs2/src/dnfs2.cpp b/dnfs2/src/dnfs2.cpp index 62fa19b..5d1071f 100644 --- a/dnfs2/src/dnfs2.cpp +++ b/dnfs2/src/dnfs2.cpp @@ -13,6 +13,7 @@ * P1-8 update_note 改 pid≠0 时校验父节点存在 * P1-9 RpcClient 启用 retry_on_refused * P1-11 note_id 显式 as_int64() + 类型校验 + * L-1 update_note 成环检测(祖先走查,拒绝移入自身/自身子树) * * 依赖: libhttp (HttpServer), libmsg (RpcClient/AsyncQueue), boost::json, * OpenSSL, libcurl @@ -42,6 +43,7 @@ #include #include #include +#include #include namespace json = boost::json; @@ -661,6 +663,49 @@ private: return std::string(); } + // L-1 修复:判定 candidate 是否位于 id 的子树内(即 id 是 candidate 的祖先)。 + // db2 execute 不支持递归查询,从 candidate 沿 pid 链逐级向上走查,途中命中 id 即成立; + // 走到根(pid=0)即无环。深度上限 + visited 集防御存量脏数据(已含环)导致的死循环, + // 走查在写库之前,命中即拒绝,不会新增脏数据。 + // db 业务错误抛 500(fail-closed,不误放行);candidate 不存在视为不在子树内 + // (调用方已单独校验新父节点存在性)。 + bool _is_descendant(int64_t id, int64_t candidate) { + constexpr int kMaxAncestorDepth = 64; + std::unordered_set visited; + int64_t cur = candidate; + for (int depth = 0; depth < kMaxAncestorDepth; ++depth) { + if (cur == id) return true; + if (cur == 0) return false; + if (!visited.insert(cur).second) return false; // 存量环,走不到根,按无环处理 + + json::object payload; + payload["action"] = "select"; + payload["table"] = "Notes"; + payload["columns"] = json::array{"pid"}; + payload["where"] = json::object{{"id", cur}}; + payload["limit"] = 1; + + DbResult r = _call_db(payload); + if (!r.success) { + _log("ERROR", "dnfs2", "成环检测查询失败 cur=" + std::to_string(cur) + + " err=" + r.error); + throw DnfsExp("查询节点失败", 500); + } + if (!r.data.is_array() || r.data.as_array().empty()) return false; + + const auto& row = r.data.as_array()[0]; + if (!row.is_object() || !row.as_object().contains("pid") + || !row.as_object().at("pid").is_int64()) { + return false; + } + cur = row.as_object().at("pid").as_int64(); + } + // 深度超限:保守拒绝,避免把潜在环写入 + _log("WARNING", "dnfs2", "成环检测深度超限 id=" + std::to_string(id) + + " candidate=" + std::to_string(candidate)); + throw DnfsExp("节点层级过深,无法完成校验", 400); + } + // 观看人数自增(get_notes 打开节点 pid 时调用)。 // db2 的 update 仅支持字面量绑定(SET col = ?),无法表达 views = views + 1, // 故采用「读当前值 → 写 +1」。读改写非原子,高并发下可能少计,作为浏览计数可接受。 @@ -1237,6 +1282,14 @@ private: throw DnfsExp("父节点不存在", 400); } + // L-1 修复:新父节点为自身或位于自身子树内会成环(子树不可达),拒绝。 + // pid=0(挂到根)天然无环;pid==id 时走查首轮即命中 + if (pid != 0 && _is_descendant(id, pid)) { + _log("WARNING", "dnfs2", "update_note 拒绝成环 id=" + std::to_string(id) + + " pid=" + std::to_string(pid)); + throw DnfsExp("不能将节点移动到自身或其子节点内", 400); + } + // 特殊节点校验:上方所有权校验已保证是节点拥有者或管理员,此处仅校验信封格式 json::value sp = parse_sp(content); if (!sp.is_null()) {