Compare commits
2
Commits
e2f05f9a43
...
edbb7ef1c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edbb7ef1c6 | ||
|
|
3ace5931e6 |
No files matched your search
+90
-31
@@ -22,6 +22,7 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@@ -58,11 +59,34 @@ static constexpr size_t kMaxInElements = 1000; // 单个 IN 运算符元素
|
|||||||
static std::string utc_now() {
|
static std::string utc_now() {
|
||||||
auto now = std::chrono::system_clock::now();
|
auto now = std::chrono::system_clock::now();
|
||||||
std::time_t t = std::chrono::system_clock::to_time_t(now);
|
std::time_t t = std::chrono::system_clock::to_time_t(now);
|
||||||
|
struct tm tmbuf;
|
||||||
|
gmtime_r(&t, &tmbuf);
|
||||||
char buf[32];
|
char buf[32];
|
||||||
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", std::gmtime(&t));
|
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", &tmbuf);
|
||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 工具:安全字符串转 int(失败时抛出带字段名的友好错误)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static int safe_stoi(const std::string& s, const char* field_name) {
|
||||||
|
try {
|
||||||
|
size_t pos = 0;
|
||||||
|
int val = std::stoi(s, &pos);
|
||||||
|
if (pos != s.size()) {
|
||||||
|
throw std::runtime_error(std::string("invalid ") + field_name
|
||||||
|
+ ": '" + s + "' (trailing characters)");
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
} catch (const std::runtime_error&) {
|
||||||
|
throw; // 已有友好信息,直接传播
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
throw std::runtime_error(std::string("invalid ") + field_name
|
||||||
|
+ ": '" + s + "' (" + e.what() + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 工具:简单 INI 解析(白名单模式)
|
// 工具:简单 INI 解析(白名单模式)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -137,7 +161,7 @@ static Config load_config(int argc, char* argv[]) {
|
|||||||
|
|
||||||
// 1. 从环境变量读取 confs 引导地址
|
// 1. 从环境变量读取 confs 引导地址
|
||||||
if (const char* h = std::getenv("CONFS_HOST")) cfg.confs_host = h;
|
if (const char* h = std::getenv("CONFS_HOST")) cfg.confs_host = h;
|
||||||
if (const char* p = std::getenv("CONFS_PORT")) cfg.confs_port = std::stoi(p);
|
if (const char* p = std::getenv("CONFS_PORT")) cfg.confs_port = safe_stoi(p, "CONFS_PORT");
|
||||||
|
|
||||||
// 2. 从 confs 服务拉取 db.ini(retry_on_refused=true,等待 confs 就绪)
|
// 2. 从 confs 服务拉取 db.ini(retry_on_refused=true,等待 confs 就绪)
|
||||||
std::string ini_text;
|
std::string ini_text;
|
||||||
@@ -183,25 +207,25 @@ static Config load_config(int argc, char* argv[]) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
cfg.listen_host = get("server", "host", cfg.listen_host);
|
cfg.listen_host = get("server", "host", cfg.listen_host);
|
||||||
cfg.listen_port = std::stoi(get("server", "port", std::to_string(cfg.listen_port)));
|
cfg.listen_port = safe_stoi(get("server", "port", std::to_string(cfg.listen_port)), "[server] port");
|
||||||
cfg.workers = std::stoi(get("server", "workers",std::to_string(cfg.workers)));
|
cfg.workers = safe_stoi(get("server", "workers",std::to_string(cfg.workers)), "[server] workers");
|
||||||
cfg.db_url = get("database", "url", cfg.db_url);
|
cfg.db_url = get("database", "url", cfg.db_url);
|
||||||
cfg.retention_days = std::stoi(get("logs", "retention_days", std::to_string(cfg.retention_days)));
|
cfg.retention_days = safe_stoi(get("logs", "retention_days", std::to_string(cfg.retention_days)), "[logs] retention_days");
|
||||||
cfg.max_records = std::stoi(get("logs", "max_records", std::to_string(cfg.max_records)));
|
cfg.max_records = safe_stoi(get("logs", "max_records", std::to_string(cfg.max_records)), "[logs] max_records");
|
||||||
cfg.cleanup_interval = std::stoi(get("logs", "cleanup_interval", std::to_string(cfg.cleanup_interval)));
|
cfg.cleanup_interval = safe_stoi(get("logs", "cleanup_interval", std::to_string(cfg.cleanup_interval)), "[logs] cleanup_interval");
|
||||||
|
|
||||||
// 4. 环境变量最高优先级覆盖(Docker 兼容)
|
// 4. 环境变量最高优先级覆盖(Docker 兼容)
|
||||||
if (const char* v = std::getenv("LISTEN_ADDR")) cfg.listen_host = v;
|
if (const char* v = std::getenv("LISTEN_ADDR")) cfg.listen_host = v;
|
||||||
if (const char* v = std::getenv("LISTEN_PORT")) cfg.listen_port = std::stoi(v);
|
if (const char* v = std::getenv("LISTEN_PORT")) cfg.listen_port = safe_stoi(v, "LISTEN_PORT");
|
||||||
if (const char* v = std::getenv("WORKERS")) cfg.workers = std::stoi(v);
|
if (const char* v = std::getenv("WORKERS")) cfg.workers = safe_stoi(v, "WORKERS");
|
||||||
if (const char* v = std::getenv("DATABASE_URL")) cfg.db_url = v;
|
if (const char* v = std::getenv("DATABASE_URL")) cfg.db_url = v;
|
||||||
|
|
||||||
// 5. 命令行参数(最高优先级)
|
// 5. 命令行参数(最高优先级)
|
||||||
for (int i = 1; i < argc; ++i) {
|
for (int i = 1; i < argc; ++i) {
|
||||||
std::string arg = argv[i];
|
std::string arg = argv[i];
|
||||||
if ((arg == "--addr" || arg == "-a") && i + 1 < argc) cfg.listen_host = argv[++i];
|
if ((arg == "--addr" || arg == "-a") && i + 1 < argc) cfg.listen_host = argv[++i];
|
||||||
else if ((arg == "--port" || arg == "-p") && i + 1 < argc) cfg.listen_port = std::stoi(argv[++i]);
|
else if ((arg == "--port" || arg == "-p") && i + 1 < argc) cfg.listen_port = safe_stoi(argv[++i], "--port");
|
||||||
else if ((arg == "--workers" || arg == "-w") && i + 1 < argc) cfg.workers = std::stoi(argv[++i]);
|
else if ((arg == "--workers" || arg == "-w") && i + 1 < argc) cfg.workers = safe_stoi(argv[++i], "--workers");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 数值配置范围校验(防误配置导致异常行为)
|
// 6. 数值配置范围校验(防误配置导致异常行为)
|
||||||
@@ -250,7 +274,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
~DbConn() {
|
~DbConn() {
|
||||||
if (_db) { sqlite3_close(_db); _db = nullptr; }
|
if (_db) { sqlite3_close_v2(_db); _db = nullptr; }
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlite3* get() const { return _db; }
|
sqlite3* get() const { return _db; }
|
||||||
@@ -318,6 +342,18 @@ public:
|
|||||||
_cleanup_running = true;
|
_cleanup_running = true;
|
||||||
_cleanup_thread = std::thread(&DB2Server::_cleanup_loop, this);
|
_cleanup_thread = std::thread(&DB2Server::_cleanup_loop, this);
|
||||||
|
|
||||||
|
// RAII 作用域守卫:无论 rpc.start() 正常返回还是抛出(socket/bind/listen 失败、
|
||||||
|
// 事件循环异常),都停止并 join 清理线程。否则异常路径下 _cleanup_thread 保持
|
||||||
|
// joinable,栈展开时 std::thread 析构会触发 std::terminate() 硬崩溃,掩盖真实错误。
|
||||||
|
struct CleanupGuard {
|
||||||
|
DB2Server& s;
|
||||||
|
~CleanupGuard() {
|
||||||
|
s._cleanup_running = false;
|
||||||
|
s._cleanup_cv.notify_all();
|
||||||
|
if (s._cleanup_thread.joinable()) s._cleanup_thread.join();
|
||||||
|
}
|
||||||
|
} guard{*this};
|
||||||
|
|
||||||
// 启动 RPC 服务
|
// 启动 RPC 服务
|
||||||
MsgServerConfig srv_cfg;
|
MsgServerConfig srv_cfg;
|
||||||
srv_cfg.workers = _cfg.workers;
|
srv_cfg.workers = _cfg.workers;
|
||||||
@@ -333,11 +369,6 @@ public:
|
|||||||
});
|
});
|
||||||
|
|
||||||
rpc.start();
|
rpc.start();
|
||||||
|
|
||||||
// rpc.start() 返回后停止清理线程
|
|
||||||
_cleanup_running = false;
|
|
||||||
_cleanup_cv.notify_all();
|
|
||||||
if (_cleanup_thread.joinable()) _cleanup_thread.join();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -447,7 +478,10 @@ private:
|
|||||||
} else if (jv.is_int64()) {
|
} else if (jv.is_int64()) {
|
||||||
sqlite3_bind_int64(stmt, idx, jv.as_int64());
|
sqlite3_bind_int64(stmt, idx, jv.as_int64());
|
||||||
} else if (jv.is_uint64()) {
|
} else if (jv.is_uint64()) {
|
||||||
sqlite3_bind_int64(stmt, idx, static_cast<sqlite3_int64>(jv.as_uint64()));
|
std::uint64_t u = jv.as_uint64();
|
||||||
|
if (u > static_cast<std::uint64_t>(INT64_MAX))
|
||||||
|
throw std::runtime_error("uint64 value exceeds signed 64-bit integer range");
|
||||||
|
sqlite3_bind_int64(stmt, idx, static_cast<sqlite3_int64>(u));
|
||||||
} else if (jv.is_double()) {
|
} else if (jv.is_double()) {
|
||||||
sqlite3_bind_double(stmt, idx, jv.as_double());
|
sqlite3_bind_double(stmt, idx, jv.as_double());
|
||||||
} else if (jv.is_bool()) {
|
} else if (jv.is_bool()) {
|
||||||
@@ -522,7 +556,9 @@ private:
|
|||||||
parts.push_back(col + " LIKE ?");
|
parts.push_back(col + " LIKE ?");
|
||||||
wc.params.push_back(_coerce_value(ci, op_val));
|
wc.params.push_back(_coerce_value(ci, op_val));
|
||||||
} else if (op == "in") {
|
} else if (op == "in") {
|
||||||
if (!op_val.is_array() || op_val.as_array().empty()) {
|
if (!op_val.is_array())
|
||||||
|
throw std::runtime_error("'in' operator requires an array value");
|
||||||
|
if (op_val.as_array().empty()) {
|
||||||
parts.push_back("0");
|
parts.push_back("0");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -570,8 +606,11 @@ private:
|
|||||||
json::value _handle_execute(const json::object& req) {
|
json::value _handle_execute(const json::object& req) {
|
||||||
// 提取公共字段
|
// 提取公共字段
|
||||||
std::string action = "select";
|
std::string action = "select";
|
||||||
if (auto it = req.find("action"); it != req.end() && it->value().is_string())
|
if (auto it = req.find("action"); it != req.end()) {
|
||||||
|
if (!it->value().is_string())
|
||||||
|
throw std::runtime_error("'action' must be a string");
|
||||||
action = std::string(it->value().as_string());
|
action = std::string(it->value().as_string());
|
||||||
|
}
|
||||||
|
|
||||||
if (!req.contains("table") || !req.at("table").is_string())
|
if (!req.contains("table") || !req.at("table").is_string())
|
||||||
throw std::runtime_error("missing required field: table");
|
throw std::runtime_error("missing required field: table");
|
||||||
@@ -585,6 +624,11 @@ private:
|
|||||||
|
|
||||||
std::vector<ColInfo> cols = _get_schema(table);
|
std::vector<ColInfo> cols = _get_schema(table);
|
||||||
|
|
||||||
|
// 'where' 存在但非对象时立即报错,不静默忽略(与 action/columns 校验一致)
|
||||||
|
if (auto wit = req.find("where"); wit != req.end() && !wit->value().is_object()) {
|
||||||
|
throw std::runtime_error("'where' must be an object");
|
||||||
|
}
|
||||||
|
|
||||||
if (action == "select") return _do_select(req, table, cols);
|
if (action == "select") return _do_select(req, table, cols);
|
||||||
if (action == "insert") return _do_insert(req, table, cols);
|
if (action == "insert") return _do_insert(req, table, cols);
|
||||||
if (action == "update") return _do_update(req, table, cols);
|
if (action == "update") return _do_update(req, table, cols);
|
||||||
@@ -608,6 +652,8 @@ private:
|
|||||||
it != req.end() && it->value().is_array() && !it->value().as_array().empty()) {
|
it != req.end() && it->value().is_array() && !it->value().as_array().empty()) {
|
||||||
col_expr.clear();
|
col_expr.clear();
|
||||||
for (auto& c : it->value().as_array()) {
|
for (auto& c : it->value().as_array()) {
|
||||||
|
if (!c.is_string())
|
||||||
|
throw std::runtime_error("columns array must contain only strings");
|
||||||
std::string col_name = std::string(c.as_string());
|
std::string col_name = std::string(c.as_string());
|
||||||
_validate_col(col_name, col_map);
|
_validate_col(col_name, col_map);
|
||||||
if (!col_expr.empty()) col_expr += ", ";
|
if (!col_expr.empty()) col_expr += ", ";
|
||||||
@@ -627,26 +673,36 @@ private:
|
|||||||
it != req.end() && it->value().is_array()) {
|
it != req.end() && it->value().is_array()) {
|
||||||
std::string order;
|
std::string order;
|
||||||
for (auto& ob : it->value().as_array()) {
|
for (auto& ob : it->value().as_array()) {
|
||||||
if (!ob.is_object()) continue;
|
if (!ob.is_object())
|
||||||
|
throw std::runtime_error("order_by array must contain only objects");
|
||||||
auto& o = ob.as_object();
|
auto& o = ob.as_object();
|
||||||
if (!o.contains("column")) continue;
|
if (!o.contains("column") || !o.at("column").is_string())
|
||||||
|
throw std::runtime_error("order_by entry requires 'column' string field");
|
||||||
std::string col_name = std::string(o.at("column").as_string());
|
std::string col_name = std::string(o.at("column").as_string());
|
||||||
_validate_col(col_name, col_map);
|
_validate_col(col_name, col_map);
|
||||||
if (!order.empty()) order += ", ";
|
if (!order.empty()) order += ", ";
|
||||||
order += col_name;
|
order += col_name;
|
||||||
if (o.contains("desc") && o.at("desc").as_bool()) order += " DESC";
|
if (o.contains("desc")) {
|
||||||
|
if (!o.at("desc").is_bool())
|
||||||
|
throw std::runtime_error("order_by 'desc' must be boolean");
|
||||||
|
if (o.at("desc").as_bool()) order += " DESC";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!order.empty()) sql += " ORDER BY " + order;
|
if (!order.empty()) sql += " ORDER BY " + order;
|
||||||
}
|
}
|
||||||
|
|
||||||
// limit / offset
|
// limit / offset(类型不符立即报错,不静默忽略,与 action 校验一致)
|
||||||
if (auto it = req.find("limit"); it != req.end() && it->value().is_int64()) {
|
if (auto it = req.find("limit"); it != req.end()) {
|
||||||
|
if (!it->value().is_int64())
|
||||||
|
throw std::runtime_error("limit must be an integer");
|
||||||
std::int64_t limit = it->value().as_int64();
|
std::int64_t limit = it->value().as_int64();
|
||||||
if (limit < 0)
|
if (limit < 0)
|
||||||
throw std::runtime_error("limit must be non-negative");
|
throw std::runtime_error("limit must be non-negative");
|
||||||
sql += " LIMIT " + std::to_string(limit);
|
sql += " LIMIT " + std::to_string(limit);
|
||||||
}
|
}
|
||||||
if (auto it = req.find("offset"); it != req.end() && it->value().is_int64()) {
|
if (auto it = req.find("offset"); it != req.end()) {
|
||||||
|
if (!it->value().is_int64())
|
||||||
|
throw std::runtime_error("offset must be an integer");
|
||||||
std::int64_t offset = it->value().as_int64();
|
std::int64_t offset = it->value().as_int64();
|
||||||
if (offset < 0)
|
if (offset < 0)
|
||||||
throw std::runtime_error("offset must be non-negative");
|
throw std::runtime_error("offset must be non-negative");
|
||||||
@@ -861,7 +917,9 @@ private:
|
|||||||
|
|
||||||
sqlite3_step(stmt);
|
sqlite3_step(stmt);
|
||||||
sqlite3_finalize(stmt);
|
sqlite3_finalize(stmt);
|
||||||
} catch (...) {}
|
} catch (const std::exception& e) {
|
||||||
|
std::fprintf(stderr, "[db2] _log_internal failed: %s\n", e.what());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 后台日志清理线程 -------------------------------------------------------
|
// ---- 后台日志清理线程 -------------------------------------------------------
|
||||||
@@ -899,19 +957,20 @@ private:
|
|||||||
if (total > _cfg.max_records) {
|
if (total > _cfg.max_records) {
|
||||||
int to_delete = total - _cfg.max_records;
|
int to_delete = total - _cfg.max_records;
|
||||||
sqlite3_finalize(stmt);
|
sqlite3_finalize(stmt);
|
||||||
|
stmt = nullptr;
|
||||||
std::string trim_sql =
|
std::string trim_sql =
|
||||||
"DELETE FROM Logs WHERE id IN "
|
"DELETE FROM Logs WHERE id IN "
|
||||||
"(SELECT id FROM Logs ORDER BY id ASC LIMIT "
|
"(SELECT id FROM Logs ORDER BY id ASC LIMIT "
|
||||||
+ std::to_string(to_delete) + ")";
|
+ std::to_string(to_delete) + ")";
|
||||||
conn.exec(trim_sql);
|
conn.exec(trim_sql);
|
||||||
trimmed = sqlite3_changes(conn.get());
|
trimmed = sqlite3_changes(conn.get());
|
||||||
} else {
|
|
||||||
sqlite3_finalize(stmt);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
sqlite3_finalize(stmt);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
std::fprintf(stderr, "[db2] cleanup COUNT prepare failed: %s\n",
|
||||||
|
sqlite3_errmsg(conn.get()));
|
||||||
}
|
}
|
||||||
|
if (stmt) sqlite3_finalize(stmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expired > 0 || trimmed > 0) {
|
if (expired > 0 || trimmed > 0) {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# db2 模块安全审计报告
|
# db2 模块安全审计报告
|
||||||
|
|
||||||
**初次审计**: 2026-08-06
|
**初次审计**: 2026-08-06
|
||||||
**最近复审**: 2026-08-07(P2-3 + P3-8 修复完成,编译验证通过)
|
**最近复审**: 2026-08-07(第六轮审计,全部问题已修复,编译验证通过)
|
||||||
**审计范围**: `db2/src/db2.cpp`、`Makefile`、`docker-entrypoint.sh`
|
**审计范围**: `db2/src/db2.cpp`、`Makefile`、`docker-entrypoint.sh`
|
||||||
**对照基准**: Python 版 `db/dbv2.py`(接口兼容性对比)
|
**对照基准**: Python 版 `db/dbv2.py`(接口兼容性对比)
|
||||||
**代码版本**: main 分支 761cfd4 + P2-3/P3-8 修复
|
**代码版本**: main 分支 + 第六轮修复
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,9 +14,9 @@
|
|||||||
|--------|------|--------|--------|
|
|--------|------|--------|--------|
|
||||||
| P0 | 2 | 2 | 0 |
|
| P0 | 2 | 2 | 0 |
|
||||||
| P1 | 1 | 1 | 0 |
|
| P1 | 1 | 1 | 0 |
|
||||||
| P2 | 3 | 3 | 0 |
|
| P2 | 4 | 4 | 0 |
|
||||||
| P3 | 11 | 7 | 4 |
|
| P3 | 20 | 20 | 0 |
|
||||||
| **合计** | **17** | **13** | **4** |
|
| **合计** | **26** | **26** | **0** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -104,11 +104,53 @@
|
|||||||
| 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-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-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-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-7** | **`utc_now()` 使用非线程安全的 `gmtime()`** | [L58-66](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L58-L66) | ✅ 已修复(2026-08-07,第五轮):改用 `gmtime_r(&t, &tmbuf)` 线程安全版本,消除多线程并发调用时的静态缓冲区数据竞争 |
|
||||||
| **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-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-9** | **`columns` 数组中非字符串元素未检查类型** | [L633-636](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L633-L636) | ✅ 已修复(2026-08-07,第五轮):添加 `if (!c.is_string()) throw "columns array must contain only strings"` 类型校验,非字符串元素抛清晰错误而非 boost 原始异常 |
|
||||||
| **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-10** | **`DbConn` 析构使用 `sqlite3_close` 而非 `sqlite3_close_v2`** | [L254-256](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L254-L256) | ✅ 已修复(2026-08-07,第五轮):改用 `sqlite3_close_v2`,异常路径存在未 finalize 的 stmt 时标记为 zombie 状态,等所有 stmt 释放后自动关闭,更鲁棒 |
|
||||||
| **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 后包装为带字段名的明确错误信息 |
|
| **P3-11** | **`std::stoi/stoll` 解析配置失败无友好错误提示** | [L72-87](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L72-L87)、[L163](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L163)、[L208-214](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L208-L214)、[L218-219](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L218-L219)、[L226-227](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L226-L227) | ✅ 已修复(2026-08-07,第五轮):引入 `safe_stoi(s, field_name)` 辅助函数,所有 `std::stoi` 调用点(CONFS_PORT、INI 配置、环境变量、命令行参数共 9 处)替换为 `safe_stoi`,失败时抛带字段名的友好错误(如 `"invalid [server] port: 'abc' (stoi)"`) |
|
||||||
|
| **P3-12**(新) | **`_log_internal` 静默吞掉所有异常** | [L898-900](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L898-L900) | ✅ 已修复(2026-08-07,第五轮):`catch (...)` 改为 `catch (const std::exception& e)`,并 `fprintf(stderr, "[db2] _log_internal failed: %s", e.what())`,便于排查日志写入失败原因,仍不阻塞业务 |
|
||||||
|
| **P3-13**(新) | **`_do_cleanup` COUNT 查询 prepare 失败静默跳过** | [L947-951](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L947-L951) | ✅ 已修复(2026-08-07,第五轮):prepare 失败时 `fprintf(stderr, ...)` 记录 sqlite3_errmsg,同时简化 finalize 逻辑为单一出口 `if (stmt) sqlite3_finalize(stmt)`,避免重复 finalize 风险 |
|
||||||
|
| **P3-14**(新) | **`order_by` 非法元素静默跳过** | [L654-671](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L654-L671) | ✅ 已修复(2026-08-07,第五轮):非 object 元素、缺少 `column` 字段、`column` 非 string、`desc` 非 bool 均改为抛 `runtime_error` 明确报错,不再静默跳过,避免掩盖客户端错误 |
|
||||||
|
| **P3-15**(新) | **`action` 字段非 string 静默默认为 "select"** | [L596-600](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L596-L600) | ✅ 已修复(2026-08-07,第五轮):`action` 字段存在但非 string 类型时抛 `"'action' must be a string"`,不再静默回退为 "select" 导致意外执行查询 |
|
||||||
|
| **P3-16**(新) | **`order_by` 的 `column`/`desc` 字段类型未校验** | [L658-668](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L658-L668) | ✅ 已修复(2026-08-07,第五轮,与 P3-14 一同):`column` 字段检查 `is_string()`,`desc` 字段检查 `is_bool()`,类型不符时抛清晰错误而非 boost 原始异常 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第六轮审计新增问题(2026-08-07)
|
||||||
|
|
||||||
|
### ND-1(P2): `start()` 异常路径导致 `std::terminate()` 硬崩溃
|
||||||
|
|
||||||
|
- **位置**: `start()` [L341-355](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L341-L355)
|
||||||
|
- **当前状态**: ✅ 已修复(2026-08-07,第六轮)
|
||||||
|
- **问题**: 清理线程 [L343](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L343) 启动后,若 `rpc.start()` 抛出异常(socket/bind/listen 失败如端口被占用,或事件循环异常经 `MsgServer::start()` 重新抛出),原有的 `_cleanup_running=false; notify; join` 正常清理路径被跳过。栈展开时 `DB2Server` 析构,`std::thread` 成员仍 joinable → `std::terminate()` 进程硬崩溃,并掩盖真实错误(本应干净退出返回错误码)。
|
||||||
|
- **修复**: 引入 RAII 作用域守卫 `CleanupGuard` [L348-355](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L348-L355),析构时停止并 join 清理线程。无论 `rpc.start()` 正常返回还是抛出,清理线程都被优雅回收,异常沿调用链传播到 `main` 的 catch 返回错误码。
|
||||||
|
|
||||||
|
### ND-4(P3): `limit`/`offset`/`where` 类型不符被静默忽略
|
||||||
|
|
||||||
|
- **位置**: `_do_select` limit/offset [L692-708](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L692-L708)、`_handle_execute` where [L626-630](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L626-L630)
|
||||||
|
- **当前状态**: ✅ 已修复(2026-08-07,第六轮)
|
||||||
|
- **问题**: `limit`/`offset` 仅当 `is_int64()` 才应用,`where` 仅当 `is_object()` 才解析;调用方传错类型(字符串/浮点/数组)时字段被静默丢弃,出现"看似生效实则未生效"的静默偏差,掩盖客户端错误。
|
||||||
|
- **修复**: `limit`/`offset` 存在但非整型、`where` 存在但非对象时抛清晰错误(与 P3-15 action 校验一致)。
|
||||||
|
|
||||||
|
### ND-5(P3): `_bind_value` uint64 超 INT64_MAX 截断溢出
|
||||||
|
|
||||||
|
- **位置**: `_bind_value` [L480-485](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L480-L485)
|
||||||
|
- **当前状态**: ✅ 已修复(2026-08-07,第六轮)
|
||||||
|
- **问题**: `jv.as_uint64()` 经 `static_cast<sqlite3_int64>` 直接截断,超出 INT64_MAX 的 uint64 值发生回绕,数值静默失真。
|
||||||
|
- **修复**: [L480-483](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L480-L483) 绑定前校验 `u > INT64_MAX` 则抛错,防止静默数值截断。
|
||||||
|
|
||||||
|
### ND-6(P3): `in` 运算符非数组值被静默当作空数组
|
||||||
|
|
||||||
|
- **位置**: `_build_where` in 分支 [L558-562](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L558-L562)
|
||||||
|
- **当前状态**: ✅ 已修复(2026-08-07,第六轮)
|
||||||
|
- **问题**: `in` 值为字符串/数字等非数组时被当作空数组 → 恒假条件,静默掩盖客户端错误。
|
||||||
|
- **修复**: [L558-559](file:///Users/qingsong/Documents/blackbird/db2/src/db2.cpp#L558-L559) 非数组立即抛 `"'in' operator requires an array value"`;仅空数组保留恒假 `0` 语义(与 Python 版 `column.in_([])` 一致)。
|
||||||
|
|
||||||
|
### 已知限制(设计决策,非缺陷)
|
||||||
|
|
||||||
|
- **ND-2(L-13)**: **`select` 无结果集上限** — 无 WHERE + 无 `limit` 时 `SELECT *` 返回全表并整体装入内存,恶意/异常客户端可触发内存耗尽 DoS。为使调用方零改动切换而不加隐式 limit,记录为已知限制;建议 gateway 侧对响应做大小/行数限制,或后续引入可配置 `max_result_rows`(默认关闭以保兼容)。
|
||||||
|
- **ND-3(L-14)**: **`update`/`delete` 无 WHERE 时全表更新/删除,无内置防护** — 与 Python 版行为对齐,属兼容性权衡;调用方漏传 where 会静默作用于全表,属数据安全风险。建议调用方侧强制校验,或后续提供可选 `require_where` 开关。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -134,6 +176,16 @@
|
|||||||
18. **日志截断** — `kMaxMessage=8192`、`kMaxExtra=4096`,防止超长日志耗尽内存
|
18. **日志截断** — `kMaxMessage=8192`、`kMaxExtra=4096`,防止超长日志耗尽内存
|
||||||
19. **不可拷贝连接** — `DbConn` 显式 `= delete` 拷贝构造/赋值,防止 double free
|
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/绑定失败等异常行为
|
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/绑定失败等异常行为
|
||||||
|
21. **线程安全时间函数** — `utc_now()` 使用 `gmtime_r` 而非 `gmtime`,消除多线程并发写日志时的静态缓冲区数据竞争(P3-7 修复)
|
||||||
|
22. **安全 sqlite3 关闭** — `DbConn` 析构使用 `sqlite3_close_v2`,异常路径未 finalize 的 stmt 不会导致连接泄漏(P3-10 修复)
|
||||||
|
23. **友好配置解析错误** — `safe_stoi(s, field_name)` 包装所有 `std::stoi` 调用点,失败时抛带字段名的错误信息(P3-11 修复)
|
||||||
|
24. **全量 JSON 字段类型校验** — `action`/`columns`/`order_by` 的 `column`+`desc` 字段均检查 JSON 类型,类型不符抛清晰错误而非 boost 原始异常(P3-9/P3-15/P3-16 修复)
|
||||||
|
25. **日志写入失败可观测** — `_log_internal` catch 块记录 stderr,不再静默吞异常(P3-12 修复)
|
||||||
|
26. **清理线程错误可观测** — `_do_cleanup` COUNT 查询 prepare 失败时记录 stderr(P3-13 修复)
|
||||||
|
27. **清理线程 RAII 守卫** — `start()` 使用 `CleanupGuard`,`rpc.start()` 异常路径下清理线程被优雅回收,不触发 `std::terminate`(ND-1 修复)
|
||||||
|
28. **select 输入类型全量校验** — `where`/`limit`/`offset` 类型不符抛清晰错误,不静默忽略(ND-4 修复)
|
||||||
|
29. **uint64 溢出防护** — `_bind_value` 绑定前校验超出 INT64_MAX 抛错,防止静默数值截断(ND-5 修复)
|
||||||
|
30. **`in` 运算符类型校验** — 非数组立即抛错,仅空数组保留恒假语义(ND-6 修复)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -152,13 +204,22 @@
|
|||||||
- [x] **P3-4**: WHERE/IN 输入规模上限
|
- [x] **P3-4**: WHERE/IN 输入规模上限
|
||||||
- [x] **P3-5**: 空 values 前置校验
|
- [x] **P3-5**: 空 values 前置校验
|
||||||
- [x] **P3-6**: 未知 WHERE 运算符报错
|
- [x] **P3-6**: 未知 WHERE 运算符报错
|
||||||
- [x] **P3-8**: 数值配置范围校验(与 P2-3 合并完成)
|
- [x] **P3-7**: `utc_now()` 改用 `gmtime_r` 保证线程安全(第五轮)
|
||||||
|
- [x] **P3-9**: `columns` 数组元素类型检查,非字符串抛清晰错误(第五轮)
|
||||||
|
- [x] **P3-10**: `DbConn` 析构改用 `sqlite3_close_v2`(第五轮)
|
||||||
|
- [x] **P3-11**: `safe_stoi` 包装所有 `std::stoi` 调用,带字段名友好错误(第五轮)
|
||||||
|
- [x] **P3-12**: `_log_internal` catch 块记录 stderr(第五轮)
|
||||||
|
- [x] **P3-13**: `_do_cleanup` COUNT prepare 失败记录 stderr(第五轮)
|
||||||
|
- [x] **P3-14**: `order_by` 非法元素抛错而非静默跳过(第五轮)
|
||||||
|
- [x] **P3-15**: `action` 非 string 抛错而非默认 "select"(第五轮)
|
||||||
|
- [x] **P3-16**: `order_by` 的 `column`/`desc` 字段类型校验(第五轮)
|
||||||
|
- [x] **ND-1**: `start()` 清理线程 RAII 守卫,`rpc.start()` 异常路径不 `std::terminate`(第六轮)
|
||||||
|
- [x] **ND-4**: `where`/`limit`/`offset` 类型不符立即抛错,不静默忽略(第六轮)
|
||||||
|
- [x] **ND-5**: `_bind_value` uint64 超 INT64_MAX 抛错,防止截断溢出(第六轮)
|
||||||
|
- [x] **ND-6**: `in` 运算符非数组值抛错,仅空数组保留恒假语义(第六轮)
|
||||||
|
|
||||||
### 待修复(建议中期内完成)
|
### 待修复
|
||||||
- [ ] **P3-7**: `utc_now()` 改用 `gmtime_r` 保证线程安全
|
无 — 全部 26 个问题已修复(100%)。新增 2 项已知限制(ND-2/ND-3)属兼容性设计决策,记录于"第六轮审计新增问题"章节。
|
||||||
- [ ] **P3-9**: `columns` 数组元素类型检查(非字符串跳过或报错)
|
|
||||||
- [ ] **P3-10**: `DbConn` 析构改用 `sqlite3_close_v2`
|
|
||||||
- [ ] **P3-11**: `std::stoi` 解析配置时 try/catch 包装为带字段名的友好错误
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -184,13 +245,41 @@
|
|||||||
6. 对照 libsqlite3 审计最佳实践,发现 `sqlite3_close` 可改进(P3-10)
|
6. 对照 libsqlite3 审计最佳实践,发现 `sqlite3_close` 可改进(P3-10)
|
||||||
7. 更新行号引用至当前代码
|
7. 更新行号引用至当前代码
|
||||||
|
|
||||||
### P2-3 / P3-8 修复验证(2026-08-07,本次)
|
### 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
|
1. 在 `load_config` 末尾(命令行参数解析之后)新增 5 项数值范围校验:port 1-65535、workers ≥1、cleanup_interval ≥1、retention_days ≥0、max_records ≥1
|
||||||
2. 非法配置立即 `fprintf` 带字段名的明确错误信息后 `exit(1)`,与已有配置必填校验风格一致
|
2. 非法配置立即 `fprintf` 带字段名的明确错误信息后 `exit(1)`,与已有配置必填校验风格一致
|
||||||
3. 修复 error handler lambda 未使用的 `this` 捕获,消除 `-Wunused-lambda-capture` 警告
|
3. 修复 error handler lambda 未使用的 `this` 捕获,消除 `-Wunused-lambda-capture` 警告
|
||||||
4. `g++ -std=c++20 -O3 -Wall -Werror` 编译通过,零警告零错误
|
4. `g++ -std=c++20 -O3 -Wall -Werror` 编译通过,零警告零错误
|
||||||
5. 更新审计文档:修复进度、问题状态、安全机制清单、修复路线图、复审结论
|
5. 更新审计文档:修复进度、问题状态、安全机制清单、修复路线图、复审结论
|
||||||
|
|
||||||
|
### 第五轮复审(2026-08-07,本次)
|
||||||
|
1. 通读当前代码全部 960 行,逐函数审查
|
||||||
|
2. 修复 4 个遗留 P3 问题:
|
||||||
|
- P3-7: `utc_now()` 改用 `gmtime_r` 线程安全版本
|
||||||
|
- P3-9: `columns` 数组元素添加 `is_string()` 类型校验
|
||||||
|
- P3-10: `DbConn` 析构改用 `sqlite3_close_v2`
|
||||||
|
- P3-11: 引入 `safe_stoi(s, field_name)` 辅助函数,替换全部 9 处 `std::stoi` 调用
|
||||||
|
3. 新发现并修复 5 个 P3 问题:
|
||||||
|
- P3-12: `_log_internal` 的 `catch (...)` 改为 `catch (const std::exception& e)` 并记录 stderr
|
||||||
|
- P3-13: `_do_cleanup` COUNT 查询 prepare 失败时记录 stderr,简化 finalize 逻辑为单一出口
|
||||||
|
- P3-14: `order_by` 非法元素(非 object、缺 column、类型不符)改为抛错而非静默跳过
|
||||||
|
- P3-15: `action` 非 string 类型时抛错而非默认 "select"
|
||||||
|
- P3-16: `order_by` 的 `column`/`desc` 字段添加 `is_string()`/`is_bool()` 类型校验
|
||||||
|
4. `clang++ -std=c++20 -O2 -Wall -Werror -c` 编译验证通过,零警告零错误
|
||||||
|
5. 更新审计文档:修复进度(22/22)、问题状态、安全机制清单(26 项)、修复路线图、复审结论
|
||||||
|
|
||||||
|
### 第六轮复审(2026-08-07,本次)
|
||||||
|
1. 通读当前代码全部约 1000 行,逐函数审查
|
||||||
|
2. 核查 libmsg `RpcServer`/`MsgServer` 依赖行为,确认 `start()` 在 socket/bind/listen 失败时抛出异常
|
||||||
|
3. 新发现并修复 4 个问题:
|
||||||
|
- ND-1 (P2): `start()` 异常路径清理线程未回收 → `std::terminate()` 硬崩溃,引入 `CleanupGuard` RAII 守卫
|
||||||
|
- ND-4 (P3): `where`/`limit`/`offset` 类型不符静默忽略
|
||||||
|
- ND-5 (P3): `_bind_value` uint64 超 INT64_MAX 截断溢出
|
||||||
|
- ND-6 (P3): `in` 运算符非数组值静默当作空数组
|
||||||
|
4. 记录 2 项已知限制 ND-2/ND-3(无结果集上限、无 WHERE 全表更新/删除,均属兼容性设计决策)
|
||||||
|
5. `clang++ -std=c++20 -O2 -Wall -Werror -fsyntax-only` 与 `g++ -std=c++20 -O3 -Wall -Werror -c` 双重编译验证通过,零警告零错误
|
||||||
|
6. 更新审计文档:修复进度(26/26)、问题状态、安全机制清单(30 项)、修复路线图、复审结论
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 并发性分析
|
## 并发性分析
|
||||||
@@ -237,12 +326,21 @@ Cleanup thread → thread_local DbConn (sqlite3*)
|
|||||||
|
|
||||||
## 复审结论
|
## 复审结论
|
||||||
|
|
||||||
**当前状态**:P0/P1/P2 级问题已全部修复(6/6),仅剩 P3 级 4 个低危健壮性改进项。SQL 注入防护机制完整,配置误操作风险已全部消除,核心安全有保障。剩余 P3 问题均不直接导致安全漏洞:
|
**当前状态**:六轮审计累计发现 **26 个问题,全部已修复(100%)**。P0/P1/P2 级问题在第六轮后全部修复(2 P0 / 1 P1 / 4 P2),第五轮修复了 9 个 P3 问题,第六轮新发现修复 1 个 P2(ND-1)与 3 个 P3(ND-4/ND-5/ND-6)。SQL 注入防护机制完整,配置误操作风险已全部消除,所有 JSON 字段访问路径均有类型校验,异常路径均有错误日志,启动异常路径不再触发进程硬崩溃。新增 2 项已知限制(ND-2/ND-3)属兼容性设计决策。
|
||||||
- P3-7 `gmtime()` 线程安全:实际数据损坏概率极低(只有多线程同时写日志时竞争静态缓冲区,时间精度只到秒)
|
|
||||||
- P3-9 columns 类型检查:仅影响错误信息友好度,不会注入或崩溃
|
|
||||||
- P3-10 `sqlite3_close_v2`:异常路径防御性改进,当前正常路径全部正确 finalize
|
|
||||||
- P3-11 友好错误信息:仅影响配置排错效率
|
|
||||||
|
|
||||||
**与 Python 版对比**:安全基线已超越 Python 版——C++ 版新增了 schema 版本感知自动失效、空 values 校验、未知运算符报错、limit/offset 非负校验、WHERE/IN 输入规模上限、表名格式校验、日志截断、配置范围校验等防护,Python 版尚未实现这些。
|
**第六轮修复亮点**:
|
||||||
|
- **启动健壮性**:`CleanupGuard` RAII 守卫确保 `rpc.start()` 无论返回还是抛出都回收清理线程,消除异常路径 `std::terminate()` 硬崩溃
|
||||||
|
- **输入类型完备**:`where`/`limit`/`offset` 类型不符立即抛错,杜绝"看似生效实则未生效"的静默偏差
|
||||||
|
- **数值保真**:`_bind_value` 对超 INT64_MAX 的 uint64 抛错,防止静默截断溢出
|
||||||
|
- **运算符严格化**:`in` 运算符非数组值抛错,仅空数组保留恒假语义(对齐 Python 版)
|
||||||
|
|
||||||
**生产可用性**:✅ **可用于生产**。当前版本在配置正确(默认配置全部合法)的前提下运行稳定。剩余 P3 问题可按需排期修复。
|
**第五轮修复亮点**:
|
||||||
|
- **线程安全**:`utc_now()` 改用 `gmtime_r`,消除多线程并发写日志时的数据竞争
|
||||||
|
- **资源安全**:`sqlite3_close_v2` 替代 `sqlite3_close`,异常路径不泄漏连接
|
||||||
|
- **错误可观测**:`_log_internal` 和 `_do_cleanup` 不再静默吞异常,均有 stderr 输出
|
||||||
|
- **输入校验完整**:`action`/`columns`/`order_by` 全部 JSON 字段类型校验,类型不符抛清晰错误
|
||||||
|
- **配置友好**:`safe_stoi` 包装全部 9 处 `std::stoi` 调用,失败时带字段名定位
|
||||||
|
|
||||||
|
**与 Python 版对比**:安全基线已超越 Python 版——C++ 版新增了 schema 版本感知自动失效、空 values 校验、未知运算符报错、limit/offset 非负与类型校验、WHERE/IN 输入规模上限、表名格式校验、日志截断、配置范围校验、线程安全时间函数、安全 sqlite3 关闭、全量 JSON 字段类型校验、异常可观测、启动异常路径 RAII 守卫等防护,Python 版尚未实现这些。
|
||||||
|
|
||||||
|
**生产可用性**:✅ **可用于生产**。全部 26 个问题已修复,30 项安全机制就位,核心安全与健壮性均有保障。2 项已知限制(ND-2 无结果集上限、ND-3 无 WHERE 全表更新/删除)为兼容性权衡,建议在调用方(gateway)侧做相应防护。
|
||||||
Reference in new issue
Block a user