- 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.
17 KiB
db2 模块安全审计报告
初次审计: 2026-08-06
最近复审: 2026-08-07(P2-3 + P3-8 修复完成,编译验证通过)
审计范围: db2/src/db2.cpp、Makefile、docker-entrypoint.sh
对照基准: Python 版 db/dbv2.py(接口兼容性对比)
代码版本: main 分支 761cfd4 + P2-3/P3-8 修复
修复进度
| 优先级 | 总数 | 已修复 | 未修复 |
|---|---|---|---|
| P0 | 2 | 2 | 0 |
| P1 | 1 | 1 | 0 |
| P2 | 3 | 3 | 0 |
| P3 | 11 | 7 | 4 |
| 合计 | 17 | 13 | 4 |
P0 — 严重问题
P0-1: 列名 SQL 注入(列名未校验直接拼接 SQL)
-
位置: 历史版本 L399-455 等位置
-
当前状态: ✅ 已修复(2026-08-06)
-
修复: 新增
_validate_col静态方法 db2.cpp L443-449,对所有列名(where keys、columns、order_by、INSERT/UPDATE values keys)校验其存在于表 schema 的col_map中,不存在则抛"unknown column"错误。5 个注入点全部修复。sqlite3_prepare_v2只编译单条语句(; DROP TABLE不会执行),但单语句内注入在修复前仍然可行。修复后列名必须是真实存在的列名,注入不可能。 -
注入点覆盖:
P0-2: 空 in 数组 → 无 WHERE 子句 → 全表删除/更新
- 位置:
_build_wherein运算符分支 - 当前状态: ✅ 已修复(2026-08-06)
- 修复: L503-506 空
in数组现在生成恒假条件parts.push_back("0"),使 WHERE 子句匹配零行,与 Python 版 SQLAlchemycolumn.in_([])行为一致。
P1 — 高危问题
P1-1: 所有值绑定为 TEXT,类型信息丢失
-
位置: 历史版本
_coerce_value/_bind_value -
当前状态: ✅ 已修复(2026-08-06)
-
修复:
_coerce_valueL414-420 现在返回boost::json::value,仅对字符串做$now替换,其他类型保持原样。- 新增
_bind_valueL422-440,按 JSON 类型分别调用sqlite3_bind_int64/sqlite3_bind_double/sqlite3_bind_text/sqlite3_bind_null/sqlite3_bind_int(bool)。 WhereClause::params改为std::vector<json::value>,保留原始类型。
修复后 INTEGER/REAL 等数值类型以原生 SQLite 存储类写入,避免 TEXT 列上的字符串比较问题,行为与 Python 版一致。
P2 — 中危问题
P2-1: 清理时间戳格式不一致(T vs 空格)导致保留期偏移约 1 天
- 位置: 历史版本
utc_now+_do_cleanup - 当前状态: ✅ 已修复(2026-08-07)
- 修复: L863-865 清理 SQL 改用
strftime('%Y-%m-%dT%H:%M:%S','now','-N days'),与utc_now()的T分隔格式完全对齐。
P2-2: 缺少配置完整性校验
- 位置: 历史版本
load_config - 当前状态: ✅ 已修复(2026-08-07)
- 修复: L159-175 在 INI 解析后,对 7 个必需字段做必填校验,缺失任一则
fprintf明确错误信息后exit(1)。
P2-3: cleanup_interval 无下限校验,配置为 0 或负数时导致清理线程 busy loop 占满 CPU
- 位置:
_cleanup_loop、load_config - 当前状态: ✅ 已修复(2026-08-07)
- 修复: db2.cpp L207-L227 在
load_config返回前(所有配置来源——INI、环境变量、命令行参数——都加载完毕后)统一增加数值范围校验,非法值立即fprintf明确错误信息后exit(1)。 - 校验项:
listen_port: 1-65535workers: ≥ 1cleanup_interval: ≥ 1(busy loop 问题根源)retention_days: ≥ 0max_records: ≥ 1
P3 — 低危问题
| # | 问题 | 位置 | 说明 |
|---|---|---|---|
| P3-1 | schema 缓存永不失效 | 历史版本 | ✅ 已修复:缓存项携带 PRAGMA schema_version L270-273,每次取 schema 前比对版本号;DDL 变更后自动失效并重新反射 |
| P3-2 | _coerce_value 对 null 返回 "" |
历史版本 | ✅ 已修复:_coerce_value 改为返回 json::value,null 保持为 null |
| P3-3 | limit/offset 未校验负值 |
历史版本 | ✅ 已修复:L622-632 非负校验 |
| P3-4 | WHERE 条件数 / IN 数组大小无上限 | 历史版本 | ✅ 已修复:L463-467 64 条件上限、L507-511 IN 元素 1000 上限 |
| P3-5 | 空 values 对象导致 INSERT/UPDATE 语法错误 |
历史版本 | ✅ 已修复:L680-682、L724-726 前置校验 |
| P3-6 | 未知 WHERE 运算符被静默跳过 | 历史版本 | ✅ 已修复:L519-521 未知运算符抛错 |
| P3-7(新) | utc_now() 使用非线程安全的 gmtime() |
L62 | ❌ 未修复:POSIX gmtime() 返回指向静态缓冲区的指针,多线程并发调用(worker线程+cleanup线程同时写日志时)存在数据竞争。虽然时间精度只到秒,实际数据损坏概率极低,但应使用线程安全版本 gmtime_r(&t, &tm) |
| P3-8(新) | 其他数值配置无范围校验 | L186-L191 | ✅ 已修复(2026-08-07,与 P2-3 一同):L207-L227 所有数值配置项范围校验覆盖:port 1-65535、workers ≥1、cleanup_interval ≥1、retention_days ≥0、max_records ≥1 |
| P3-9(新) | columns 数组中非字符串元素未检查类型 |
L588-589 | ❌ 未修复: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 | ❌ 未修复:若异常路径存在未 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、L186-L191、L195-L197、L203-L204 | ❌ 未修复:配置项(端口、workers、数值日志参数)为非法数字时,std::stoi 抛出 std::invalid_argument,异常 what() 仅显示 "stoi",无法定位哪个配置项出错,排查困难。建议 try/catch 后包装为带字段名的明确错误信息 |
已有安全机制(正确实现)
- 表名校验 — L558-562:仅允许
[A-Za-z0-9_],防止表名级 SQL 注入 - 列名白名单校验 —
_validate_colL443-449:5 个拼接点全部覆盖,列名必须存在于表 schema - 值参数化绑定 —
_bind_valueL422-440:按 JSON 类型分别sqlite3_bind_*,无值级 SQL 注入 - per-thread 连接 — L337-341:
thread_local独占连接,避免跨线程共享 prepared statement - 异常脱敏 —
RpcServer::_sanitize_error(libmsg):正则替换绝对路径为<redacted> - 仅操作已有表 —
PRAGMA table_info反射,无法建表/删表/执行 DDL - 空
in恒假条件 — L503-506:空数组不忽略 WHERE,匹配零行 - SQLite PRAGMA 优化 — L223-228:WAL + synchronous=NORMAL + foreign_keys=ON + busy_timeout=5000 + mmap_size=256MB
- schema 缓存加锁 + 版本感知失效 —
_schema_mtxL326 保护缓存;PRAGMA schema_version自动感知 DDL 变更 - 配置完整性校验 — L159-175:7 个必需字段缺失则
exit(1) - confs 引导重试 —
retry_on_refused=true,不要求服务启动顺序 - 日志内部直写 —
_log_internalL814-843 绕过 RPC 防递归,异常静默不阻塞业务 - 后台清理线程 — 条件变量优雅停止 L847-856,时间+条数双策略清理,时间戳格式 T 对齐
- 空 values 前置校验 — INSERT/UPDATE 空 values 立即报错
- WHERE 运算符白名单 — 未知运算符立即抛错,不静默跳过
limit/offset非负校验 — 负值直接报错- WHERE / IN 输入规模上限 — 64 条件 / 1000 IN 元素,防资源耗尽
- 日志截断 —
kMaxMessage=8192、kMaxExtra=4096,防止超长日志耗尽内存 - 不可拷贝连接 —
DbConn显式= delete拷贝构造/赋值,防止 double free - 数值配置范围校验 — L207-L227:listen_port 1-65535、workers ≥1、cleanup_interval ≥1、retention_days ≥0、max_records ≥1,非法值立即
exit(1),防止误配置导致 busy loop/绑定失败等异常行为
修复路线图
已完成
- P0-1: 列名白名单校验
- P0-2: 空
in生成恒假条件 - P1-1: 按 JSON 类型分别参数绑定
- P2-1: 清理时间戳格式统一为 T 分隔
- P2-2: 配置完整性必填校验
- P2-3 + P3-8: 所有数值配置项范围校验(port 1-65535、workers ≥1、cleanup_interval ≥1、retention_days ≥0、max_records ≥1)
- P3-1: schema 缓存基于
schema_version自动失效 - P3-2:
_coerce_valuenull 正确处理 - P3-3: limit/offset 非负校验
- P3-4: WHERE/IN 输入规模上限
- P3-5: 空 values 前置校验
- P3-6: 未知 WHERE 运算符报错
- 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)
- 通读
db2/src/db2.cpp全部 851 行实现 - 逐函数对照 Python 版
db/dbv2.py验证接口兼容性 - 审查 libmsg RpcServer 异常脱敏机制
- 逐注入点构造 PoC 验证 SQL 注入可行性
第一次复审(2026-08-07)
- 通读修复后代码 875 行
- 验证 P2-1/P2-2 修复
- 全量扫描发现 P3-5/P3-6
第二次复审(2026-08-07)
- 通读当前代码(main 分支 761cfd4)
- 逐函数验证所有 11 个已修复问题无回退
- 检查所有
std::stoi/数值转换路径,发现配置无范围校验问题(P2-3、P3-8、P3-11) - 检查所有系统调用线程安全性,发现
gmtime()问题(P3-7) - 检查所有 JSON 访问路径的类型安全,发现
columns缺类型检查(P3-9) - 对照 libsqlite3 审计最佳实践,发现
sqlite3_close可改进(P3-10) - 更新行号引用至当前代码
P2-3 / P3-8 修复验证(2026-08-07,本次)
- 在
load_config末尾(命令行参数解析之后)新增 5 项数值范围校验:port 1-65535、workers ≥1、cleanup_interval ≥1、retention_days ≥0、max_records ≥1 - 非法配置立即
fprintf带字段名的明确错误信息后exit(1),与已有配置必填校验风格一致 - 修复 error handler lambda 未使用的
this捕获,消除-Wunused-lambda-capture警告 g++ -std=c++20 -O3 -Wall -Werror编译通过,零警告零错误- 更新审计文档:修复进度、问题状态、安全机制清单、修复路线图、复审结论
并发性分析
并发模型
RpcServer (N workers, 默认 4)
├── thread 1 → thread_local DbConn (sqlite3*)
├── thread 2 → thread_local DbConn (sqlite3*)
├── thread 3 → thread_local DbConn (sqlite3*)
└── thread 4 → thread_local DbConn (sqlite3*)
Cleanup thread → thread_local DbConn (sqlite3*)
每个 worker 和清理线程各自持有独立的 sqlite3* 连接(thread_local),通过 WAL 模式实现并发读写。
优势
| 机制 | 位置 | 效果 |
|---|---|---|
| WAL 模式 | L223 | 读不阻塞写、写不阻塞读 |
| per-thread 连接 | L337-341 | 无跨线程共享,零连接池锁 |
| busy_timeout=5000 | L226 | 写-写冲突自动等锁最多 5 秒 |
| 无 GIL | — | C++ 线程真并行 |
瓶颈与风险
- 写-写串行化 — SQLite 同一时刻只允许一个写事务,典型写入 300-800 TPS,对当前规模足够
- 无批量事务 — 每条请求独立 auto-commit,无原子批量操作(Python 版同)
- 清理与业务写竞争 — 大批量 DELETE 持锁数百毫秒可能阻塞业务写入
_schema_mtx全局锁 — 仅首次反射表结构时持锁,缓存命中无锁,影响极小
与 Python 版对比
| 维度 | Python db (dbv2.py) | C++ db2 |
|---|---|---|
| 并发模型 | N worker + 共享 engine | N worker + per-thread 连接 |
| GIL 限制 | 有 | 无,真并行 |
| 连接管理 | SQLAlchemy 连接池 | thread_local 独占 |
| 写冲突处理 | SQLAlchemy 自动重试 | busy_timeout=5000 |
复审结论
当前状态: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 问题可按需排期修复。