fix(dnfs2): update_note 成环检测(修复已知限制 L-1)
新增 _is_descendant 祖先走查:改 pid 时从新父节点沿 pid 链逐级向上, 命中自身即拒绝(自引用/移入自身子树均成环,子树不可达);深度上限 64 + visited 集防存量脏数据死循环,db 错误 fail-closed 抛 500。 README 已知限制 L-1 移入安全修复表。
This commit is contained in:
1 parent
e9463a28fb
commit
9b6aa39b94
2 files changed
+54
-1
No files matched your search
+1
-1
@@ -71,10 +71,10 @@ dnfs2 :8006 ──pymsg──► db2 :8005 (execute 路由) ──► SQLite
|
|||||||
| P1-8 | update_note 改 pid 未校验父节点 | pid≠0 时校验父节点存在 |
|
| P1-8 | update_note 改 pid 未校验父节点 | pid≠0 时校验父节点存在 |
|
||||||
| P1-9 | db RPC 未启用启动等待 | `RpcClient(retry_on_refused=true)` |
|
| P1-9 | db RPC 未启用启动等待 | `RpcClient(retry_on_refused=true)` |
|
||||||
| P1-11 | note_id 无强制类型转换 | 显式 `as_int64()` + 类型校验 |
|
| P1-11 | note_id 无强制类型转换 | 显式 `as_int64()` + 类型校验 |
|
||||||
|
| L-1 | update_note 允许创建循环 | `_is_descendant` 祖先走查:拒绝新父节点为自身或位于自身子树内(深度上限 64,防存量环死循环) |
|
||||||
|
|
||||||
## 已知限制(P2,设计决策不修复)
|
## 已知限制(P2,设计决策不修复)
|
||||||
|
|
||||||
- L-1: update_note 允许创建循环(需图遍历检测后代关系)
|
|
||||||
- L-2: delete_note TOCTOU 残余(需 db2 事务支持)
|
- L-2: delete_note TOCTOU 残余(需 db2 事务支持)
|
||||||
- L-4: 无速率限制(gateway/WAF 职责)
|
- L-4: 无速率限制(gateway/WAF 职责)
|
||||||
- L-5: title/content 无最大长度校验
|
- L-5: title/content 无最大长度校验
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
* P1-8 update_note 改 pid≠0 时校验父节点存在
|
* P1-8 update_note 改 pid≠0 时校验父节点存在
|
||||||
* P1-9 RpcClient 启用 retry_on_refused
|
* P1-9 RpcClient 启用 retry_on_refused
|
||||||
* P1-11 note_id 显式 as_int64() + 类型校验
|
* P1-11 note_id 显式 as_int64() + 类型校验
|
||||||
|
* L-1 update_note 成环检测(祖先走查,拒绝移入自身/自身子树)
|
||||||
*
|
*
|
||||||
* 依赖: libhttp (HttpServer), libmsg (RpcClient/AsyncQueue), boost::json,
|
* 依赖: libhttp (HttpServer), libmsg (RpcClient/AsyncQueue), boost::json,
|
||||||
* OpenSSL, libcurl
|
* OpenSSL, libcurl
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace json = boost::json;
|
namespace json = boost::json;
|
||||||
@@ -661,6 +663,49 @@ private:
|
|||||||
return std::string();
|
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<int64_t> 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 时调用)。
|
// 观看人数自增(get_notes 打开节点 pid 时调用)。
|
||||||
// db2 的 update 仅支持字面量绑定(SET col = ?),无法表达 views = views + 1,
|
// db2 的 update 仅支持字面量绑定(SET col = ?),无法表达 views = views + 1,
|
||||||
// 故采用「读当前值 → 写 +1」。读改写非原子,高并发下可能少计,作为浏览计数可接受。
|
// 故采用「读当前值 → 写 +1」。读改写非原子,高并发下可能少计,作为浏览计数可接受。
|
||||||
@@ -1237,6 +1282,14 @@ private:
|
|||||||
throw DnfsExp("父节点不存在", 400);
|
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);
|
json::value sp = parse_sp(content);
|
||||||
if (!sp.is_null()) {
|
if (!sp.is_null()) {
|
||||||
|
|||||||
Reference in new issue
Block a user