From 9256a8157b00cf1c3f53d5d5d8ef66d697034020 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Fri, 7 Aug 2026 20:48:22 +0800 Subject: [PATCH] fix(db2): add numeric config range validation (fix P2-3/P3-8) - Add range checks in load_config() for all numeric config values: * listen_port: 1-65535 * workers: >= 1 * cleanup_interval: >= 1 (prevents busy loop DoS when misconfigured to 0/negative) * retention_days: >= 0 * max_records: >= 1 - Fix unused lambda capture 'this' in RpcServer error handler (-Wunused-lambda-capture with -Wall -Werror) - Update security audit doc: mark P2-3/P3-8 fixed, mark production ready Verified: g++ -std=c++20 -O3 -Wall -Werror compiles with zero warnings. --- db2/src/db2.cpp | 24 +- docs/security/db2_SECURITY_AUDIT_20260806.md | 301 ++++++++----------- 2 files changed, 148 insertions(+), 177 deletions(-) diff --git a/db2/src/db2.cpp b/db2/src/db2.cpp index ca12772..c2d16bd 100644 --- a/db2/src/db2.cpp +++ b/db2/src/db2.cpp @@ -204,6 +204,28 @@ static Config load_config(int argc, char* argv[]) { else if ((arg == "--workers" || arg == "-w") && i + 1 < argc) cfg.workers = std::stoi(argv[++i]); } + // 6. 数值配置范围校验(防误配置导致异常行为) + if (cfg.listen_port < 1 || cfg.listen_port > 65535) { + std::fprintf(stderr, "[db2] invalid listen_port: %d (must be 1-65535)\n", cfg.listen_port); + std::exit(1); + } + if (cfg.workers < 1) { + std::fprintf(stderr, "[db2] invalid workers: %d (must be >= 1)\n", cfg.workers); + std::exit(1); + } + if (cfg.cleanup_interval < 1) { + std::fprintf(stderr, "[db2] invalid cleanup_interval: %d (must be >= 1 second)\n", cfg.cleanup_interval); + std::exit(1); + } + if (cfg.retention_days < 0) { + std::fprintf(stderr, "[db2] invalid retention_days: %d (must be >= 0)\n", cfg.retention_days); + std::exit(1); + } + if (cfg.max_records < 1) { + std::fprintf(stderr, "[db2] invalid max_records: %d (must be >= 1)\n", cfg.max_records); + std::exit(1); + } + return cfg; } @@ -301,7 +323,7 @@ public: srv_cfg.workers = _cfg.workers; RpcServer rpc(_cfg.listen_host, _cfg.listen_port, srv_cfg, - [this](const std::string& route, const std::exception& e) { + [](const std::string& route, const std::exception& e) { std::fprintf(stderr, "[db2] handler error route=%s: %s\n", route.c_str(), e.what()); }); diff --git a/docs/security/db2_SECURITY_AUDIT_20260806.md b/docs/security/db2_SECURITY_AUDIT_20260806.md index ac51299..e286eb5 100644 --- a/docs/security/db2_SECURITY_AUDIT_20260806.md +++ b/docs/security/db2_SECURITY_AUDIT_20260806.md @@ -1,10 +1,10 @@ # db2 模块安全审计报告 **初次审计**: 2026-08-06 -**最近复审**: 2026-08-07(P2 修复后全量重审,新增 P3-5/P3-6) -**审计范围**: `db2/src/db2.cpp`(全部实现 926 行)、`Makefile`、`docker-entrypoint.sh` +**最近复审**: 2026-08-07(P2-3 + P3-8 修复完成,编译验证通过) +**审计范围**: `db2/src/db2.cpp`、`Makefile`、`docker-entrypoint.sh` **对照基准**: Python 版 `db/dbv2.py`(接口兼容性对比) -**代码版本**: main 分支 ec626dc + P3 全量修复 +**代码版本**: main 分支 761cfd4 + P2-3/P3-8 修复 --- @@ -14,9 +14,9 @@ |--------|------|--------|--------| | P0 | 2 | 2 | 0 | | P1 | 1 | 1 | 0 | -| P2 | 2 | 2 | 0 | -| P3 | 6 | 6 | 0 | -| **合计** | **11** | **11** | **0** | +| P2 | 3 | 3 | 0 | +| P3 | 11 | 7 | 4 | +| **合计** | **17** | **13** | **4** | --- @@ -24,48 +24,26 @@ ### P0-1: 列名 SQL 注入(列名未校验直接拼接 SQL) -- **位置**: `db2.cpp` L399-455(where keys)、L518-523(columns)、L536-544(order_by)、L609-619(INSERT col_list)、L654-663(UPDATE set_clause) +- **位置**: 历史版本 L399-455 等位置 - **当前状态**: ✅ 已修复(2026-08-06) -- **修复**: 新增 `_validate_col` 静态方法,对所有列名(where keys、columns、order_by、INSERT/UPDATE values keys)校验其存在于表 schema 的 `col_map` 中,不存在则抛 `"unknown column"` 错误。5 个注入点全部修复。 +- **修复**: 新增 `_validate_col` 静态方法 [db2.cpp L443-449](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L443-L449),对所有列名(where keys、columns、order_by、INSERT/UPDATE values keys)校验其存在于表 schema 的 `col_map` 中,不存在则抛 `"unknown column"` 错误。5 个注入点全部修复。 - `sqlite3_prepare_v2` 只编译单条语句(`; DROP TABLE` 不会执行),但**单语句内注入**仍然可行: + `sqlite3_prepare_v2` 只编译单条语句(`; DROP TABLE` 不会执行),但**单语句内注入**在修复前仍然可行。修复后列名必须是真实存在的列名,注入不可能。 - ```json - // 攻击 1:DELETE 全表 - {"action": "delete", "table": "Users", "where": {"1=1 OR 1": 1}} - // → DELETE FROM Users WHERE 1=1 OR 1 = '1' ← 删除全部行 - - // 攻击 2:SELECT 数据泄露(UNION 注入) - {"action": "select", "table": "Users", - "columns": ["password_hash FROM Users UNION SELECT secret FROM Secrets--"]} - // → SELECT password_hash FROM Users UNION SELECT secret FROM Secrets-- FROM Users - - // 攻击 3:ORDER BY 子查询泄露 - {"action": "select", "table": "Users", - "order_by": [{"column": "(SELECT password_hash FROM Users LIMIT 1)"}]} - // → SELECT * FROM Users ORDER BY (SELECT password_hash FROM Users LIMIT 1) - ``` - - 各注入点汇总: - - | 注入点 | 代码位置 | 拼接方式 | - |--------|----------|----------| - | `where` 的 key | L399-452 | `parts.push_back(col + " = ?")` | - | `columns` 数组元素 | L518-523 | `col_expr += std::string(c.as_string())` | - | `order_by[].column` | L536-544 | `order += std::string(o.at("column").as_string())` | - | `values` 的 key (INSERT) | L609-619 | `col_list += std::string(k)` | - | `values` 的 key (UPDATE) | L654-663 | `set_clause += std::string(k) + " = ?"` | - -- **与 Python 版差异**: Python 版通过 SQLAlchemy 的 `table.columns[col]` 解析列名,列不存在时抛 `KeyError`,不可能注入。C++ 版缺失此校验,是**安全回归**。 -- **修复建议**: 对所有列名做白名单校验——要么与表名同样校验 `[A-Za-z0-9_]`,要么严格检查列名存在于 `col_map` 中(不存在则抛错)。最稳妥的做法:两者都做。 +- **注入点覆盖**: + - `where` 的 key → `_build_where` [L473-475](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L473-L475) + - `columns` 数组元素 → `_do_select` [L588-590](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L588-L590) + - `order_by[].column` → `_do_select` [L611-612](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L611-L612) + - INSERT `values` 的 key → `_do_insert` [L691-693](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L691-L693) + - UPDATE SET `values` 的 key → `_do_update` [L734-736](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L734-L736) --- ### P0-2: 空 `in` 数组 → 无 WHERE 子句 → 全表删除/更新 -- **位置**: `db2.cpp` L439 +- **位置**: `_build_where` `in` 运算符分支 - **当前状态**: ✅ 已修复(2026-08-06) -- **修复**: 空 `in` 数组现在生成恒假条件 `parts.push_back("0")`,使 WHERE 子句匹配零行,与 Python 版 SQLAlchemy `column.in_([])` 行为一致。 +- **修复**: [L503-506](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L503-L506) 空 `in` 数组现在生成恒假条件 `parts.push_back("0")`,使 WHERE 子句匹配零行,与 Python 版 SQLAlchemy `column.in_([])` 行为一致。 --- @@ -73,12 +51,12 @@ ### P1-1: 所有值绑定为 TEXT,类型信息丢失 -- **位置**: `db2.cpp` L368-392(`_coerce_value` / `_bind_value`)、L476-480(`_bind_params`) +- **位置**: 历史版本 `_coerce_value` / `_bind_value` - **当前状态**: ✅ 已修复(2026-08-06) - **修复**: - - `_coerce_value` 现在返回 `boost::json::value`,仅对字符串做 `$now` 替换,其他类型保持原样。 - - 新增 `_bind_value`,按 JSON 类型分别调用 `sqlite3_bind_int64` / `sqlite3_bind_double` / `sqlite3_bind_text` / `sqlite3_bind_null`。 - - `WhereClause::params` 改为 `std::vector`,删除原来的 `is_null` 向量;`_bind_params` 统一调用 `_bind_value`。 + - `_coerce_value` [L414-420](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L414-L420) 现在返回 `boost::json::value`,仅对字符串做 `$now` 替换,其他类型保持原样。 + - 新增 `_bind_value` [L422-440](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L422-L440),按 JSON 类型分别调用 `sqlite3_bind_int64` / `sqlite3_bind_double` / `sqlite3_bind_text` / `sqlite3_bind_null` / `sqlite3_bind_int` (bool)。 + - `WhereClause::params` 改为 `std::vector`,保留原始类型。 修复后 INTEGER/REAL 等数值类型以原生 SQLite 存储类写入,避免 TEXT 列上的字符串比较问题,行为与 Python 版一致。 @@ -88,40 +66,31 @@ ### P2-1: 清理时间戳格式不一致(T vs 空格)导致保留期偏移约 1 天 -- **位置**: `db2.cpp` L54-60(`utc_now`)、L787-789(`_do_cleanup`) +- **位置**: 历史版本 `utc_now` + `_do_cleanup` - **当前状态**: ✅ 已修复(2026-08-07) -- **修复**: 清理 SQL 改用 `strftime('%Y-%m-%dT%H:%M:%S','now','-N days')`,与 `utc_now()` 的 `T` 分隔格式完全对齐,截止时间字符串比较正确,保留期精确匹配配置天数。 - -- **原始问题**: - - 存储的时间戳用 `utc_now()`,格式 `%Y-%m-%dT%H:%M:%S`(T 分隔)。清理 SQL 用 SQLite 内置 `datetime('now', '-N days')`,格式 `%Y-%m-%d %H:%M:%S`(空格分隔): - - ```cpp - "DELETE FROM Logs WHERE ts < datetime('now', '-" - + std::to_string(_cfg.retention_days) + " days')" - ``` - - SQLite 字符串比较时 `T`(0x54) > 空格(0x20),导致与截止日同日期的记录 `ts < cutoff` 为 false,**不会被删除**。实际保留期比配置多约 1 天。 - -- **与 Python 版差异**: Python 版两端都用 `datetime.isoformat()`(T 分隔),一致。 -- **修复建议**: 用 `strftime('%Y-%m-%dT%H:%M:%S','now',-N days)` 或在 C++ 端算截止时间后参数绑定。 +- **修复**: [L863-865](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L863-L865) 清理 SQL 改用 `strftime('%Y-%m-%dT%H:%M:%S','now','-N days')`,与 `utc_now()` 的 `T` 分隔格式完全对齐。 --- ### P2-2: 缺少配置完整性校验 -- **位置**: `db2.cpp` L131-185(`load_config`) +- **位置**: 历史版本 `load_config` - **当前状态**: ✅ 已修复(2026-08-07) -- **修复**: 在 INI 解析后、环境变量覆盖前,对 7 个必需字段(`[server] host/port/workers`、`[database] url`、`[logs] retention_days/max_records/cleanup_interval`)做必填校验,缺失任一则 `fprintf` 明确错误信息后 `exit(1)`,不再用错误默认值静默运行。 +- **修复**: [L159-175](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L159-L175) 在 INI 解析后,对 7 个必需字段做必填校验,缺失任一则 `fprintf` 明确错误信息后 `exit(1)`。 -- **原始问题**: +--- - `load_config` 对缺失配置项直接用内置默认值兜底(`get(sec, key, cfg.xxx)` 模式)。Python 版(`dbv2.py` L118-129)校验所有必需配置项,缺失则退出。 +### P2-3: `cleanup_interval` 无下限校验,配置为 0 或负数时导致清理线程 busy loop 占满 CPU - 后果:confs 提供的 `db.ini` 拥缺字段时 db2 不会报错,可能用错误的默认值静默运行(如 `db_url` 缺失则用 `sqlite:///database.db`)。 - -- **与 Python 版差异**: Python 版严格校验,C++ 版宽松兜底。 -- **修复建议**: 对 `[server] host/port/workers`、`[database] url`、`[logs]` 三项做必填校验,缺失则 `exit(1)`。 +- **位置**: `_cleanup_loop`、`load_config` +- **当前状态**: ✅ 已修复(2026-08-07) +- **修复**: [db2.cpp L207-L227](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L207-L227) 在 `load_config` 返回前(所有配置来源——INI、环境变量、命令行参数——都加载完毕后)统一增加数值范围校验,非法值立即 `fprintf` 明确错误信息后 `exit(1)`。 +- **校验项**: + - `listen_port`: 1-65535 + - `workers`: ≥ 1 + - `cleanup_interval`: ≥ 1(busy loop 问题根源) + - `retention_days`: ≥ 0 + - `max_records`: ≥ 1 --- @@ -129,95 +98,98 @@ | # | 问题 | 位置 | 说明 | |---|------|------|------| -| P3-1 | schema 缓存永不失效 | L341-392 | ✅ 已修复:缓存项携带 `PRAGMA schema_version`,每次取 schema 前比对版本号;DDL 变更后自动失效并重新反射,无需重启 db2 | -| P3-2 | `_coerce_value` 对 null 返回 `""`(注释误导) | L387-393 | ✅ 已修复(2026-08-06):`_coerce_value` 改为返回 `json::value`,null 保持为 null,不再返回空字符串;`is_null` 向量已删除 | -| P3-3 | `limit`/`offset` 未校验负值 | L620-632 | ✅ 已修复:提取 `limit`/`offset` 后校验非负,负值直接抛出 `"limit/offset must be non-negative"` | -| P3-4 | WHERE 条件数 / IN 数组大小无上限 | L458-512 | ✅ 已修复:`_build_where` 入口校验 `where.size() <= 64`,`in` 分支校验数组元素 `<= 1000`,超限立即报错 | -| P3-5 | 空 `values` 对象导致 INSERT/UPDATE 语法错误 | L628-671 | ✅ 已修复(2026-08-07):`_do_insert` 和 `_do_update` 新增 `vals.empty()` 前置校验,空 `values` 直接抛出 `"insert/update requires non-empty 'values'"` 错误,不再生成无效 SQL | -| P3-6 | 未知 WHERE 运算符被静默跳过 | L446-485 | ✅ 已修复(2026-08-07):运算符分支末尾新增 `else throw "unknown operator: X"`,非法运算符立即报错,不再静默跳过导致条件失效 | +| P3-1 | schema 缓存永不失效 | 历史版本 | ✅ 已修复:缓存项携带 `PRAGMA schema_version` [L270-273](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L270-L273),每次取 schema 前比对版本号;DDL 变更后自动失效并重新反射 | +| P3-2 | `_coerce_value` 对 null 返回 `""` | 历史版本 | ✅ 已修复:`_coerce_value` 改为返回 `json::value`,null 保持为 null | +| P3-3 | `limit`/`offset` 未校验负值 | 历史版本 | ✅ 已修复:[L622-632](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L622-L632) 非负校验 | +| P3-4 | WHERE 条件数 / IN 数组大小无上限 | 历史版本 | ✅ 已修复:[L463-467](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L463-L467) 64 条件上限、[L507-511](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L507-L511) IN 元素 1000 上限 | +| P3-5 | 空 `values` 对象导致 INSERT/UPDATE 语法错误 | 历史版本 | ✅ 已修复:[L680-682](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L680-L682)、[L724-726](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L724-L726) 前置校验 | +| P3-6 | 未知 WHERE 运算符被静默跳过 | 历史版本 | ✅ 已修复:[L519-521](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L519-L521) 未知运算符抛错 | +| **P3-7**(新) | **`utc_now()` 使用非线程安全的 `gmtime()`** | [L62](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L62) | ❌ 未修复:POSIX `gmtime()` 返回指向静态缓冲区的指针,多线程并发调用(worker线程+cleanup线程同时写日志时)存在数据竞争。虽然时间精度只到秒,实际数据损坏概率极低,但应使用线程安全版本 `gmtime_r(&t, &tm)` | +| **P3-8**(新) | **其他数值配置无范围校验** | [L186-L191](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L186-L191) | ✅ 已修复(2026-08-07,与 P2-3 一同):[L207-L227](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L207-L227) 所有数值配置项范围校验覆盖:port 1-65535、workers ≥1、cleanup_interval ≥1、retention_days ≥0、max_records ≥1 | +| **P3-9**(新) | **`columns` 数组中非字符串元素未检查类型** | [L588-589](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L588-L589) | ❌ 未修复:`columns` 数组包含非字符串元素(数字、对象、null)时,直接调用 `c.as_string()` 抛出 boost::json 原始异常,错误信息不友好(对比 `order_by` 分支有 `if (!ob.is_object()) continue;` 防御)。建议增加:`if (!c.is_string()) continue;` 或显式校验类型抛清晰错误 | +| **P3-10**(新) | **`DbConn` 析构使用 `sqlite3_close` 而非 `sqlite3_close_v2`** | [L231](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L231) | ❌ 未修复:若异常路径存在未 finalize 的 prepared statement,`sqlite3_close` 返回 `SQLITE_BUSY` 导致连接泄漏;`sqlite3_close_v2` 标记连接为 zombie,等所有 stmt 释放后自动关闭,更鲁棒(与 libsqlite3 审计 P0-2 修复建议一致)。当前所有正常路径都正确 finalize 了 stmt,但改用 close_v2 是防御性最佳实践 | +| **P3-11**(新) | **`std::stoi/stoll` 解析配置失败无友好错误提示** | [L140](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L140)、[L186-L191](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L186-L191)、[L195-L197](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L195-L197)、[L203-L204](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L203-L204) | ❌ 未修复:配置项(端口、workers、数值日志参数)为非法数字时,`std::stoi` 抛出 `std::invalid_argument`,异常 `what()` 仅显示 "stoi",无法定位哪个配置项出错,排查困难。建议 try/catch 后包装为带字段名的明确错误信息 | --- ## 已有安全机制(正确实现) -1. **表名校验** — `db2.cpp` L518-522:仅允许 `[A-Za-z0-9_]`,防止表名级 SQL 注入 -2. **列名白名单校验** — `_validate_col` L416-422:所有列名必须在表 schema 中存在,5 个拼接点全部覆盖 -3. **值参数化绑定** — `_bind_value` L395-413:所有 value 按 JSON 类型分别 `sqlite3_bind_*`,无值级 SQL 注入 -4. **per-thread 连接** — L327-331:`thread_local` 连接,避免跨线程共享 prepared statement +1. **表名校验** — [L558-562](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L558-L562):仅允许 `[A-Za-z0-9_]`,防止表名级 SQL 注入 +2. **列名白名单校验** — `_validate_col` [L443-449](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L443-L449):5 个拼接点全部覆盖,列名必须存在于表 schema +3. **值参数化绑定** — `_bind_value` [L422-440](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L422-L440):按 JSON 类型分别 `sqlite3_bind_*`,无值级 SQL 注入 +4. **per-thread 连接** — [L337-341](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L337-L341):`thread_local` 独占连接,避免跨线程共享 prepared statement 5. **异常脱敏** — `RpcServer::_sanitize_error`(libmsg):正则替换绝对路径为 `` -6. **仅操作已有表** — `PRAGMA table_info` 反射,无法建表/删表 -7. **空 `in` 恒假条件** — L470-473:空 `in` 数组生成 `"0"` 使 WHERE 匹配零行 -8. **SQLite PRAGMA 优化** — WAL + synchronous=NORMAL + foreign_keys=ON + busy_timeout=5000 + mmap_size=256MB,与 Python 版一致 -9. **schema 缓存加锁 + 版本感知失效** — `_schema_mtx` L316 保护 `_schema_cache`;缓存项携带 `PRAGMA schema_version`,每次取 schema 前比对版本号,DDL 变更后自动重新反射 -10. **配置完整性校验** — L155-171:7 个必需字段缺失则 `exit(1)`,不再用默认值静默运行 -11. **confs 引导重试** — `retry_on_refused=true`,不要求启动顺序 -12. **日志内部直写** — `_log_internal` L762-791 绕过 RPC 防递归,异常静默不阻塞业务 -13. **后台清理线程** — 条件变量优雅停止,时间戳格式 T 对齐,按时间+条数双策略清理 -14. **空 values 前置校验** — `_do_insert`/`_do_update` 检测空 `values` 立即报错,不生成无效 SQL -15. **WHERE 运算符白名单** — 未知运算符立即抛出 `"unknown operator"` 错误,不再静默跳过 -16. **`limit`/`offset` 非负校验** — 负值直接报错,避免 SQLite 负 LIMIT 返回全表的意外行为 -17. **WHERE / IN 输入规模上限** — 单请求最多 64 个 WHERE 条件、单个 `IN` 最多 1000 个元素,防止恶意大请求耗尽资源 +6. **仅操作已有表** — `PRAGMA table_info` 反射,无法建表/删表/执行 DDL +7. **空 `in` 恒假条件** — [L503-506](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L503-L506):空数组不忽略 WHERE,匹配零行 +8. **SQLite PRAGMA 优化** — [L223-228](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L223-L228):WAL + synchronous=NORMAL + foreign_keys=ON + busy_timeout=5000 + mmap_size=256MB +9. **schema 缓存加锁 + 版本感知失效** — `_schema_mtx` [L326](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L326) 保护缓存;`PRAGMA schema_version` 自动感知 DDL 变更 +10. **配置完整性校验** — [L159-175](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L159-L175):7 个必需字段缺失则 `exit(1)` +11. **confs 引导重试** — `retry_on_refused=true`,不要求服务启动顺序 +12. **日志内部直写** — `_log_internal` [L814-843](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L814-L843) 绕过 RPC 防递归,异常静默不阻塞业务 +13. **后台清理线程** — 条件变量优雅停止 [L847-856](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L847-L856),时间+条数双策略清理,时间戳格式 T 对齐 +14. **空 values 前置校验** — INSERT/UPDATE 空 values 立即报错 +15. **WHERE 运算符白名单** — 未知运算符立即抛错,不静默跳过 +16. **`limit`/`offset` 非负校验** — 负值直接报错 +17. **WHERE / IN 输入规模上限** — 64 条件 / 1000 IN 元素,防资源耗尽 +18. **日志截断** — `kMaxMessage=8192`、`kMaxExtra=4096`,防止超长日志耗尽内存 +19. **不可拷贝连接** — `DbConn` 显式 `= delete` 拷贝构造/赋值,防止 double free +20. **数值配置范围校验** — [L207-L227](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L207-L227):listen_port 1-65535、workers ≥1、cleanup_interval ≥1、retention_days ≥0、max_records ≥1,非法值立即 `exit(1)`,防止误配置导致 busy loop/绑定失败等异常行为 --- ## 修复路线图 -### 立即(24小时) -- [x] **P0-1**: 列名白名单校验(校验列名在 `col_map` 中存在 + `[A-Za-z0-9_]` 格式校验) +### 已完成 +- [x] **P0-1**: 列名白名单校验 - [x] **P0-2**: 空 `in` 生成恒假条件 - -### 短期(72小时) -- [x] **P1-1**: 按 JSON 类型分别使用 `sqlite3_bind_int64` / `sqlite3_bind_double` / `sqlite3_bind_text` -- [x] **P2-2**: 配置完整性校验(必需字段缺失则退出) - -### 中期 +- [x] **P1-1**: 按 JSON 类型分别参数绑定 - [x] **P2-1**: 清理时间戳格式统一为 T 分隔 -- [x] **P3-1**: schema 缓存失效机制(基于 `PRAGMA schema_version` 自动失效并重新反射) +- [x] **P2-2**: 配置完整性必填校验 +- [x] **P2-3 + P3-8**: 所有数值配置项范围校验(port 1-65535、workers ≥1、cleanup_interval ≥1、retention_days ≥0、max_records ≥1) +- [x] **P3-1**: schema 缓存基于 `schema_version` 自动失效 +- [x] **P3-2**: `_coerce_value` null 正确处理 - [x] **P3-3**: limit/offset 非负校验 -- [x] **P3-4**: WHERE 条件数 / IN 数组大小上限(64 / 1000) -- [x] **P3-5**: 空 `values` 对象前置校验(`values` 为空时直接返回错误,避免生成无效 SQL) -- [x] **P3-6**: 未知 WHERE 运算符校验(遇到非合法运算符时抛出错误而非静默跳过) +- [x] **P3-4**: WHERE/IN 输入规模上限 +- [x] **P3-5**: 空 values 前置校验 +- [x] **P3-6**: 未知 WHERE 运算符报错 +- [x] **P3-8**: 数值配置范围校验(与 P2-3 合并完成) + +### 待修复(建议中期内完成) +- [ ] **P3-7**: `utc_now()` 改用 `gmtime_r` 保证线程安全 +- [ ] **P3-9**: `columns` 数组元素类型检查(非字符串跳过或报错) +- [ ] **P3-10**: `DbConn` 析构改用 `sqlite3_close_v2` +- [ ] **P3-11**: `std::stoi` 解析配置时 try/catch 包装为带字段名的友好错误 --- ## 审计方法 ### 初次审计(2026-08-06) - 1. 通读 `db2/src/db2.cpp` 全部 851 行实现 -2. 逐函数对照 Python 版 `db/dbv2.py` 验证接口兼容性与行为差异 -3. 审查 `libmsg/inc/us/RpcServer.hpp`/`RpcClient.hpp` 确认框架层异常脱敏机制 -4. 审查 `confs/configs/db.ini` 确认配置链 -5. 审查 `docker-entrypoint.sh` 确认 schema 初始化 -6. 逐注入点构造 PoC 请求验证 SQL 注入可行性(DELETE 全表、UNION 数据泄露、ORDER BY 子查询) -7. 追踪空 `in` 数组代码路径确认全表删除行为 +2. 逐函数对照 Python 版 `db/dbv2.py` 验证接口兼容性 +3. 审查 libmsg RpcServer 异常脱敏机制 +4. 逐注入点构造 PoC 验证 SQL 注入可行性 -### 复审(2026-08-07) +### 第一次复审(2026-08-07) +1. 通读修复后代码 875 行 +2. 验证 P2-1/P2-2 修复 +3. 全量扫描发现 P3-5/P3-6 -1. 通读修复后 `db2/src/db2.cpp` 全部 875 行实现 -2. 验证 P2-1 修复:确认 `strftime('%Y-%m-%dT%H:%M:%S','now',-N days)` 与 `utc_now()` 格式对齐 -3. 验证 P2-2 修复:确认 7 个必需字段校验逻辑正确,`confs/configs/db.ini` 包含全部必需字段 -4. 确认 P0-1/P0-2/P1-1/P3-2 修复在代码中仍然正确,无回退 -5. 全量逐函数扫描发现 P3-5(空 values)、P3-6(未知运算符)两个新既有问题 -6. 对照 Python `dbv2.py` 确认新增问题在两版中行为一致(非回归) -7. 更新安全机制清单行号引用至当前代码 +### 第二次复审(2026-08-07) +1. 通读当前代码(main 分支 761cfd4) +2. 逐函数验证所有 11 个已修复问题无回退 +3. 检查所有 `std::stoi`/数值转换路径,发现配置无范围校验问题(P2-3、P3-8、P3-11) +4. 检查所有系统调用线程安全性,发现 `gmtime()` 问题(P3-7) +5. 检查所有 JSON 访问路径的类型安全,发现 `columns` 缺类型检查(P3-9) +6. 对照 libsqlite3 审计最佳实践,发现 `sqlite3_close` 可改进(P3-10) +7. 更新行号引用至当前代码 -### 本次 P3-1 修复验证 - -1. 审查 `SchemaCacheEntry` 结构,确认缓存项同时保存 `schema_version` 与 `cols` -2. 确认 `_get_schema_version` 通过 `PRAGMA schema_version` 读取当前数据库 schema 版本 -3. 确认 `_get_schema` 在加锁前读取当前版本,缓存命中时严格比对版本号;版本不一致则重新执行 `PRAGMA table_info` 并更新缓存 -4. 编译通过,无新增告警 -5. 更新审计文档:修复进度、P3-1 状态、修复路线图、安全机制清单、并发性分析与复审结论 - -### 本次 P3-3 / P3-4 修复验证 - -1. 审查 `_do_select` 中 `limit`/`offset` 分支,确认提取 `int64` 值后校验 `>= 0`,负值抛 `"limit/offset must be non-negative"` -2. 审查 `_build_where` 入口,确认校验 `where.size() <= kMaxWhereConditions`(64),超限抛 `"too many where conditions"` -3. 审查 `in` 运算符分支,确认校验数组元素 `<= kMaxInElements`(1000),超限抛 `"too many elements in 'in' operator"` -4. 确认常量 `kMaxWhereConditions` / `kMaxInElements` 位于文件顶部,便于统一调整 -5. 编译通过,无新增告警 -6. 更新审计文档:修复进度、P3-3/P3-4 状态、修复路线图、安全机制清单与复审结论 +### P2-3 / P3-8 修复验证(2026-08-07,本次) +1. 在 `load_config` 末尾(命令行参数解析之后)新增 5 项数值范围校验:port 1-65535、workers ≥1、cleanup_interval ≥1、retention_days ≥0、max_records ≥1 +2. 非法配置立即 `fprintf` 带字段名的明确错误信息后 `exit(1)`,与已有配置必填校验风格一致 +3. 修复 error handler lambda 未使用的 `this` 捕获,消除 `-Wunused-lambda-capture` 警告 +4. `g++ -std=c++20 -O3 -Wall -Werror` 编译通过,零警告零错误 +5. 更新审计文档:修复进度、问题状态、安全机制清单、修复路线图、复审结论 --- @@ -235,65 +207,42 @@ RpcServer (N workers, 默认 4) Cleanup thread → thread_local DbConn (sqlite3*) ``` -每个 worker 持有独立的 `sqlite3*` 连接(`thread_local`,L327-331),通过 WAL 模式实现并发读写。 +每个 worker 和清理线程各自持有独立的 `sqlite3*` 连接(`thread_local`),通过 WAL 模式实现并发读写。 ### 优势 | 机制 | 位置 | 效果 | |------|------|------| -| **WAL 模式** | L219 | 读不阻塞写、写不阻塞读,多读并发零争用 | -| **per-thread 连接** | L327-331 | 无跨线程共享 prepared statement,无需连接池锁 | -| **busy_timeout=5000** | L222 | 写-写冲突时等锁最多 5 秒,短暂突发写入可自动串行化 | -| **synchronous=NORMAL** | L220 | WAL 下减少 fsync,写吞吐提升约 2-3 倍(对比 FULL) | -| **mmap_size=256MB** | L223 | 热数据走内存映射,读性能接近 memcpy | -| **schema 缓存加锁** | L316 | 仅首次请求做 `PRAGMA table_info`,后续锁外快路径命中缓存 | -| **无 GIL** | — | C++ 线程真并行,不受 Python GIL 限制 | - -典型场景表现: -- **读密集**(社区首页、用户查询):4 worker 并发读无争用,SQLite 读延迟 ~0.01ms,吞吐可达数万 QPS -- **读写混合**(dnfs 增删节点 + 查询):WAL 允许读写并行,偶尔写-写冲突由 busy_timeout 消化 +| **WAL 模式** | [L223](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L223) | 读不阻塞写、写不阻塞读 | +| **per-thread 连接** | [L337-341](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L337-L341) | 无跨线程共享,零连接池锁 | +| **busy_timeout=5000** | [L226](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L226) | 写-写冲突自动等锁最多 5 秒 | +| **无 GIL** | — | C++ 线程真并行 | ### 瓶颈与风险 - -**1. 写-写串行化** - -SQLite 任何时刻只允许一个写事务。4 个 worker 同时写时,后到者排队等锁: - -``` -thread 1: BEGIN → INSERT → COMMIT (持有写锁 ~0.1ms) -thread 2: BEGIN → INSERT → 等锁... → COMMIT -thread 3: BEGIN → INSERT → 等锁... → COMMIT -``` - -- `busy_timeout=5000` 兜底:等锁超 5 秒则抛 `SQLITE_BUSY`,返回 error 响应 -- 实际影响:单表写入吞吐约 300-800 TPS(SQLite 典型值),对本项目(日志写入 + 偶尔 CRUD)足够 - -**2. 无事务包装的批量操作** - -当前每条 execute 请求是独立的 auto-commit 事务。若客户端需要"插入 100 条要么全成功要么全失败",db2 不支持——每条独立提交,中途失败无法回滚前序行。Python 版同样如此(既有设计限制)。 - -**3. `_log_internal` 与业务写竞争** - -后台清理线程的 `DELETE FROM Logs`(L806-851)和业务 worker 的 `INSERT INTO Logs` 可能同时争用写锁。清理操作涉及大量删除,持锁时间可能达数百毫秒,期间业务写入被阻塞。 - -**4. `_schema_mtx` 锁粒度** - -全局互斥锁保护整个 `_schema_cache`。极端高并发(数百 QPS 首次请求不同表)时可能短暂排队,但实际影响极低(缓存命中后锁外快路径,仅首次 miss 持锁)。 +1. **写-写串行化** — SQLite 同一时刻只允许一个写事务,典型写入 300-800 TPS,对当前规模足够 +2. **无批量事务** — 每条请求独立 auto-commit,无原子批量操作(Python 版同) +3. **清理与业务写竞争** — 大批量 DELETE 持锁数百毫秒可能阻塞业务写入 +4. **`_schema_mtx` 全局锁** — 仅首次反射表结构时持锁,缓存命中无锁,影响极小 ### 与 Python 版对比 | 维度 | Python db (dbv2.py) | C++ db2 | |------|---------------------|---------| -| 并发模型 | RpcServer N worker 线程 + 共享 engine | RpcServer N worker 线程 + per-thread sqlite3* | -| 连接管理 | SQLAlchemy 连接池(`check_same_thread=False`) | thread_local 独占连接 | +| 并发模型 | N worker + 共享 engine | N worker + per-thread 连接 | +| GIL 限制 | 有 | 无,真并行 | +| 连接管理 | SQLAlchemy 连接池 | thread_local 独占 | | 写冲突处理 | SQLAlchemy 自动重试 | busy_timeout=5000 | -| 事务控制 | `conn.commit()` 显式提交 | auto-commit(每条隐式事务) | -| GIL 影响 | Python GIL 限制真正并行 | C++ 无 GIL,真并行 | - -**结论**:db2 的并发性优于 Python 版——无 GIL 限制、无连接池锁开销、per-thread 连接零共享。在当前项目规模下(日志写入 + 业务 CRUD,峰值百级 QPS)完全够用。若未来写入量级跃升至千级 TPS,需考虑:① 日志写入改批量事务 ② 清理线程错峰执行 ③ 或迁移至 PostgreSQL。 --- ## 复审结论 -P0/P1/P2/P3 级问题已全部修复(11/11)。当前版本安全基线已超越 Python 版(C++ 版新增了 schema 缓存失效、空 values 校验、未知运算符校验、limit/offset 非负校验、WHERE/IN 输入规模上限,Python 版尚无),可用于生产部署。 +**当前状态**:P0/P1/P2 级问题已全部修复(6/6),仅剩 P3 级 4 个低危健壮性改进项。SQL 注入防护机制完整,配置误操作风险已全部消除,核心安全有保障。剩余 P3 问题均不直接导致安全漏洞: +- P3-7 `gmtime()` 线程安全:实际数据损坏概率极低(只有多线程同时写日志时竞争静态缓冲区,时间精度只到秒) +- P3-9 columns 类型检查:仅影响错误信息友好度,不会注入或崩溃 +- P3-10 `sqlite3_close_v2`:异常路径防御性改进,当前正常路径全部正确 finalize +- P3-11 友好错误信息:仅影响配置排错效率 + +**与 Python 版对比**:安全基线已超越 Python 版——C++ 版新增了 schema 版本感知自动失效、空 values 校验、未知运算符报错、limit/offset 非负校验、WHERE/IN 输入规模上限、表名格式校验、日志截断、配置范围校验等防护,Python 版尚未实现这些。 + +**生产可用性**:✅ **可用于生产**。当前版本在配置正确(默认配置全部合法)的前提下运行稳定。剩余 P3 问题可按需排期修复。