Compare commits

...
6 Commits
Author SHA1 Message Date
qing daf4bbe6c2 docs(index): 补充 db2 审计报告与 README 索引 2026-08-06 18:26:12 +00:00
qing 00421182e4 fix(db2): 按 JSON 类型分别绑定 SQLite 参数,修复类型丢失
- _coerce_value 改为返回 json::value,保留 int/double/bool/null 原始类型
- 新增 _bind_value,按类型调用 sqlite3_bind_int64/double/text/null
- WhereClause::params 改为 vector<json::value>,删除 is_null 向量
- INSERT/UPDATE/SELECT/DELETE 统一使用按类型绑定

修复 P1-1(所有值绑定为 TEXT)和 P3-2(_coerce_value null 处理误导)
2026-08-06 18:23:30 +00:00
qing cb31db1078 docs(security): 更新 libsqlite3 审计报告修复状态
- P0-3 参数化 exec 已修复
- P1-2/P1-3/P1-4 绑定类型与异常信息已修复
- P2-2/P2-3 读取接口与文档已修复
- 修正 P2 问题数量统计(6 项而非 5 项)
2026-08-06 18:14:01 +00:00
qing 48714b88af fix(libsqlite3): 增加参数化 exec 与完整绑定/读取接口
- 新增模板 exec(sql, args...) 参数化执行,支持 int / int64_t / double /
  std::string / const char* / nullptr / std::vector<unsigned char>
- 扩展 Sqlite3_stmt:bind_int64、bind_double、bind_blob、bind_null、execute
- 增加 read_int64、read_double,read_text 补充生命周期注释
- 构造/打开异常携带 sqlite3_errmsg 详情
- 原 exec(sql) 增加硬编码 SQL 警告注释
2026-08-06 18:13:52 +00:00
qing 1c55443d43 docs(security): 新增 libsqlite3 安全审计报告
- 记录 P0/P1/P2 级别问题与修复状态
- 已修复 double free / 移动语义、close_v2、const 引用等 5 项问题
2026-08-06 18:07:14 +00:00
qing 985ff47c84 fix(libsqlite3): 修复 double free 与移动语义缺陷
- 为 Sqlite3 与 Sqlite3_stmt 显式 delete 拷贝、实现 noexcept 移动构造/赋值
- 析构函数判空后释放,防止重复释放与 use-after-free
- ~Sqlite3() 改用 sqlite3_close_v2,确保所有 statement 释放后再关闭
- stmt() 参数改为 const std::string&,支持字符串字面量
- 成员指针显式初始化为 nullptr,errMsg 初始化为 nullptr
2026-08-06 18:07:06 +00:00
5 changed files with 592 additions and 72 deletions

No files matched your search

+63 -58
View File
@@ -364,27 +364,49 @@ private:
|| n.find("date") != std::string::npos; || n.find("date") != std::string::npos;
} }
// 将 JSON 值转换为 SQLite 绑定字符串($now 替换为 UTC 时间) // 将 JSON 值做必要转换($now 替换为 UTC 时间),但保留原始类型用于绑定
static std::string _coerce_value(const ColInfo& ci, const json::value& jv) { static json::value _coerce_value(const ColInfo& ci, const json::value& jv) {
if (jv.is_string()) { if (jv.is_string()) {
std::string s(jv.as_string()); std::string s(jv.as_string());
if (_is_datetime_col(ci) && s == "$now") return utc_now(); if (_is_datetime_col(ci) && s == "$now") return json::value(utc_now());
return s;
} }
if (jv.is_int64()) return std::to_string(jv.as_int64()); return jv;
if (jv.is_uint64()) return std::to_string(jv.as_uint64()); }
if (jv.is_double()) return std::to_string(jv.as_double());
if (jv.is_bool()) return jv.as_bool() ? "1" : "0"; static void _bind_value(sqlite3_stmt* stmt, int idx, const json::value& jv) {
if (jv.is_null()) return ""; // 绑定 NULL if (jv.is_null()) {
return json::serialize(jv); sqlite3_bind_null(stmt, idx);
} else if (jv.is_int64()) {
sqlite3_bind_int64(stmt, idx, jv.as_int64());
} else if (jv.is_uint64()) {
sqlite3_bind_int64(stmt, idx, static_cast<sqlite3_int64>(jv.as_uint64()));
} else if (jv.is_double()) {
sqlite3_bind_double(stmt, idx, jv.as_double());
} else if (jv.is_bool()) {
sqlite3_bind_int(stmt, idx, jv.as_bool() ? 1 : 0);
} else if (jv.is_string()) {
std::string s(jv.as_string());
sqlite3_bind_text(stmt, idx, s.c_str(), -1, SQLITE_TRANSIENT);
} else {
std::string s = json::serialize(jv);
sqlite3_bind_text(stmt, idx, s.c_str(), -1, SQLITE_TRANSIENT);
}
}
// 校验列名:必须存在于表 schema 中,拒绝未知列名(防 SQL 注入)
static ColInfo _validate_col(const std::string& col,
const std::unordered_map<std::string, ColInfo>& col_map) {
auto it = col_map.find(col);
if (it == col_map.end())
throw std::runtime_error("unknown column: " + col);
return it->second;
} }
// ---- WHERE 子句构建 -------------------------------------------------------- // ---- WHERE 子句构建 --------------------------------------------------------
struct WhereClause { struct WhereClause {
std::string sql; // "col1 = ? AND col2 > ?" std::string sql; // "col1 = ? AND col2 > ?"
std::vector<std::string> params; // 绑定参数值(顺序对应 ?) std::vector<json::value> params; // 绑定参数值(顺序对应 ?),保留 JSON 类型
std::vector<bool> is_null; // 对应参数是否绑定 NULL
}; };
WhereClause _build_where(const json::object& where, WhereClause _build_where(const json::object& where,
@@ -398,10 +420,7 @@ private:
for (auto& [key, val] : where) { for (auto& [key, val] : where) {
std::string col(key); std::string col(key);
ColInfo ci; ColInfo ci = _validate_col(col, col_map);
auto it = col_map.find(col);
if (it != col_map.end()) ci = it->second;
else { ci.name = col; ci.type = "TEXT"; }
if (val.is_object()) { if (val.is_object()) {
// {"op": value} 形式 // {"op": value} 形式
@@ -410,39 +429,34 @@ private:
if (op == "eq") { if (op == "eq") {
parts.push_back(col + " = ?"); parts.push_back(col + " = ?");
wc.params.push_back(_coerce_value(ci, op_val)); wc.params.push_back(_coerce_value(ci, op_val));
wc.is_null.push_back(op_val.is_null());
} else if (op == "ne") { } else if (op == "ne") {
parts.push_back(col + " != ?"); parts.push_back(col + " != ?");
wc.params.push_back(_coerce_value(ci, op_val)); wc.params.push_back(_coerce_value(ci, op_val));
wc.is_null.push_back(op_val.is_null());
} else if (op == "gt") { } else if (op == "gt") {
parts.push_back(col + " > ?"); parts.push_back(col + " > ?");
wc.params.push_back(_coerce_value(ci, op_val)); wc.params.push_back(_coerce_value(ci, op_val));
wc.is_null.push_back(false);
} else if (op == "lt") { } else if (op == "lt") {
parts.push_back(col + " < ?"); parts.push_back(col + " < ?");
wc.params.push_back(_coerce_value(ci, op_val)); wc.params.push_back(_coerce_value(ci, op_val));
wc.is_null.push_back(false);
} else if (op == "ge") { } else if (op == "ge") {
parts.push_back(col + " >= ?"); parts.push_back(col + " >= ?");
wc.params.push_back(_coerce_value(ci, op_val)); wc.params.push_back(_coerce_value(ci, op_val));
wc.is_null.push_back(false);
} else if (op == "le") { } else if (op == "le") {
parts.push_back(col + " <= ?"); parts.push_back(col + " <= ?");
wc.params.push_back(_coerce_value(ci, op_val)); wc.params.push_back(_coerce_value(ci, op_val));
wc.is_null.push_back(false);
} else if (op == "like") { } else if (op == "like") {
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));
wc.is_null.push_back(false);
} else if (op == "in") { } else if (op == "in") {
if (!op_val.is_array() || op_val.as_array().empty()) continue; if (!op_val.is_array() || op_val.as_array().empty()) {
parts.push_back("0");
continue;
}
std::string placeholders; std::string placeholders;
for (auto& item : op_val.as_array()) { for (auto& item : op_val.as_array()) {
if (!placeholders.empty()) placeholders += ", "; if (!placeholders.empty()) placeholders += ", ";
placeholders += "?"; placeholders += "?";
wc.params.push_back(_coerce_value(ci, item)); wc.params.push_back(_coerce_value(ci, item));
wc.is_null.push_back(item.is_null());
} }
parts.push_back(col + " IN (" + placeholders + ")"); parts.push_back(col + " IN (" + placeholders + ")");
} }
@@ -451,7 +465,6 @@ private:
// 直接等于 // 直接等于
parts.push_back(col + " = ?"); parts.push_back(col + " = ?");
wc.params.push_back(_coerce_value(ci, val)); wc.params.push_back(_coerce_value(ci, val));
wc.is_null.push_back(val.is_null());
} }
} }
@@ -465,15 +478,9 @@ private:
// ---- 参数绑定辅助 ---------------------------------------------------------- // ---- 参数绑定辅助 ----------------------------------------------------------
static void _bind_params(sqlite3_stmt* stmt, int start_idx, static void _bind_params(sqlite3_stmt* stmt, int start_idx,
const std::vector<std::string>& params, const std::vector<json::value>& params) {
const std::vector<bool>& is_null) {
for (size_t i = 0; i < params.size(); ++i) { for (size_t i = 0; i < params.size(); ++i) {
int idx = start_idx + static_cast<int>(i); _bind_value(stmt, start_idx + static_cast<int>(i), params[i]);
if (is_null[i]) {
sqlite3_bind_null(stmt, idx);
} else {
sqlite3_bind_text(stmt, idx, params[i].c_str(), -1, SQLITE_TRANSIENT);
}
} }
} }
@@ -511,14 +518,19 @@ private:
const std::vector<ColInfo>& cols) { const std::vector<ColInfo>& cols) {
DbConn& conn = _get_conn(); DbConn& conn = _get_conn();
std::unordered_map<std::string, ColInfo> col_map;
for (auto& c : cols) col_map[c.name] = c;
// columns // columns
std::string col_expr = "*"; std::string col_expr = "*";
if (auto it = req.find("columns"); if (auto it = req.find("columns");
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()) {
std::string col_name = std::string(c.as_string());
_validate_col(col_name, col_map);
if (!col_expr.empty()) col_expr += ", "; if (!col_expr.empty()) col_expr += ", ";
col_expr += std::string(c.as_string()); col_expr += col_name;
} }
} }
@@ -537,8 +549,10 @@ private:
if (!ob.is_object()) continue; if (!ob.is_object()) continue;
auto& o = ob.as_object(); auto& o = ob.as_object();
if (!o.contains("column")) continue; if (!o.contains("column")) continue;
std::string col_name = std::string(o.at("column").as_string());
_validate_col(col_name, col_map);
if (!order.empty()) order += ", "; if (!order.empty()) order += ", ";
order += std::string(o.at("column").as_string()); order += col_name;
if (o.contains("desc") && o.at("desc").as_bool()) order += " DESC"; if (o.contains("desc") && o.at("desc").as_bool()) order += " DESC";
} }
if (!order.empty()) sql += " ORDER BY " + order; if (!order.empty()) sql += " ORDER BY " + order;
@@ -554,7 +568,7 @@ private:
if (sqlite3_prepare_v2(conn.get(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { if (sqlite3_prepare_v2(conn.get(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
throw std::runtime_error(std::string("prepare failed: ") + sqlite3_errmsg(conn.get())); throw std::runtime_error(std::string("prepare failed: ") + sqlite3_errmsg(conn.get()));
} }
_bind_params(stmt, 1, wc.params, wc.is_null); _bind_params(stmt, 1, wc.params);
// 获取结果列名(可能是 * 展开后的实际列名) // 获取结果列名(可能是 * 展开后的实际列名)
int ncols = sqlite3_column_count(stmt); int ncols = sqlite3_column_count(stmt);
@@ -603,19 +617,15 @@ private:
for (auto& c : cols) col_map[c.name] = c; for (auto& c : cols) col_map[c.name] = c;
std::string col_list, ph_list; std::string col_list, ph_list;
std::vector<std::string> params; std::vector<json::value> params;
std::vector<bool> is_null;
for (auto& [k, v] : vals) { for (auto& [k, v] : vals) {
std::string col_name(k);
ColInfo ci = _validate_col(col_name, col_map);
if (!col_list.empty()) { col_list += ", "; ph_list += ", "; } if (!col_list.empty()) { col_list += ", "; ph_list += ", "; }
col_list += std::string(k); col_list += col_name;
ph_list += "?"; ph_list += "?";
ColInfo ci;
auto it = col_map.find(std::string(k));
if (it != col_map.end()) ci = it->second;
else { ci.name = std::string(k); ci.type = "TEXT"; }
params.push_back(_coerce_value(ci, v)); params.push_back(_coerce_value(ci, v));
is_null.push_back(v.is_null());
} }
std::string sql = "INSERT INTO " + table + " (" + col_list + ") VALUES (" + ph_list + ")"; std::string sql = "INSERT INTO " + table + " (" + col_list + ") VALUES (" + ph_list + ")";
@@ -623,7 +633,7 @@ private:
if (sqlite3_prepare_v2(conn.get(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) if (sqlite3_prepare_v2(conn.get(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
throw std::runtime_error(std::string("prepare failed: ") + sqlite3_errmsg(conn.get())); throw std::runtime_error(std::string("prepare failed: ") + sqlite3_errmsg(conn.get()));
_bind_params(stmt, 1, params, is_null); _bind_params(stmt, 1, params);
int rc = sqlite3_step(stmt); int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) if (rc != SQLITE_DONE)
@@ -648,18 +658,14 @@ private:
for (auto& c : cols) col_map[c.name] = c; for (auto& c : cols) col_map[c.name] = c;
std::string set_clause; std::string set_clause;
std::vector<std::string> params; std::vector<json::value> params;
std::vector<bool> is_null;
for (auto& [k, v] : vals) { for (auto& [k, v] : vals) {
std::string col_name(k);
ColInfo ci = _validate_col(col_name, col_map);
if (!set_clause.empty()) set_clause += ", "; if (!set_clause.empty()) set_clause += ", ";
set_clause += std::string(k) + " = ?"; set_clause += col_name + " = ?";
ColInfo ci;
auto it = col_map.find(std::string(k));
if (it != col_map.end()) ci = it->second;
else { ci.name = std::string(k); ci.type = "TEXT"; }
params.push_back(_coerce_value(ci, v)); params.push_back(_coerce_value(ci, v));
is_null.push_back(v.is_null());
} }
std::string sql = "UPDATE " + table + " SET " + set_clause; std::string sql = "UPDATE " + table + " SET " + set_clause;
@@ -669,14 +675,13 @@ private:
if (!wc.sql.empty()) { if (!wc.sql.empty()) {
sql += " WHERE " + wc.sql; sql += " WHERE " + wc.sql;
params.insert(params.end(), wc.params.begin(), wc.params.end()); params.insert(params.end(), wc.params.begin(), wc.params.end());
is_null.insert(is_null.end(), wc.is_null.begin(), wc.is_null.end());
} }
sqlite3_stmt* stmt = nullptr; sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(conn.get(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) if (sqlite3_prepare_v2(conn.get(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
throw std::runtime_error(std::string("prepare failed: ") + sqlite3_errmsg(conn.get())); throw std::runtime_error(std::string("prepare failed: ") + sqlite3_errmsg(conn.get()));
_bind_params(stmt, 1, params, is_null); _bind_params(stmt, 1, params);
int rc = sqlite3_step(stmt); int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) if (rc != SQLITE_DONE)
@@ -703,7 +708,7 @@ private:
if (sqlite3_prepare_v2(conn.get(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) if (sqlite3_prepare_v2(conn.get(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
throw std::runtime_error(std::string("prepare failed: ") + sqlite3_errmsg(conn.get())); throw std::runtime_error(std::string("prepare failed: ") + sqlite3_errmsg(conn.get()));
_bind_params(stmt, 1, wc.params, wc.is_null); _bind_params(stmt, 1, wc.params);
int rc = sqlite3_step(stmt); int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) if (rc != SQLITE_DONE)
+2
View File
@@ -40,6 +40,7 @@ docs/
| gateway | [gateway_SECURITY_AUDIT_20260801.md](security/gateway_SECURITY_AUDIT_20260801.md) | C++20 应用网关(鉴权、Token、反向代理) | | gateway | [gateway_SECURITY_AUDIT_20260801.md](security/gateway_SECURITY_AUDIT_20260801.md) | C++20 应用网关(鉴权、Token、反向代理) |
| core | [core_SECURITY_AUDIT_20260801.md](security/core_SECURITY_AUDIT_20260801.md) | C++20 进程托管服务(PMC/Nidus) | | core | [core_SECURITY_AUDIT_20260801.md](security/core_SECURITY_AUDIT_20260801.md) | C++20 进程托管服务(PMC/Nidus) |
| db | [db_SECURITY_AUDIT_20260801.md](security/db_SECURITY_AUDIT_20260801.md) | Python 数据库服务(pymsg、SQLAlchemy、日志收集) | | db | [db_SECURITY_AUDIT_20260801.md](security/db_SECURITY_AUDIT_20260801.md) | Python 数据库服务(pymsg、SQLAlchemy、日志收集) |
| db2 | [db2_SECURITY_AUDIT_20260806.md](security/db2_SECURITY_AUDIT_20260806.md) | C++20 数据库 RPC 服务(db 的 C++ 重实现,SQLite C API) |
| pymsg | [pymsg_SECURITY_AUDIT_20260801.md](security/pymsg_SECURITY_AUDIT_20260801.md) | Python RPC 基础库(长度前缀帧、MsgClient/Server) | | pymsg | [pymsg_SECURITY_AUDIT_20260801.md](security/pymsg_SECURITY_AUDIT_20260801.md) | Python RPC 基础库(长度前缀帧、MsgClient/Server) |
| dnfs | [dnfs_SECURITY_AUDIT_20260801.md](security/dnfs_SECURITY_AUDIT_20260801.md) | Python 树状菜单服务 | | dnfs | [dnfs_SECURITY_AUDIT_20260801.md](security/dnfs_SECURITY_AUDIT_20260801.md) | Python 树状菜单服务 |
| static | [static_SECURITY_AUDIT_20260801.md](security/static_SECURITY_AUDIT_20260801.md) | Python 前端托管/文件服务 | | static | [static_SECURITY_AUDIT_20260801.md](security/static_SECURITY_AUDIT_20260801.md) | Python 前端托管/文件服务 |
@@ -84,6 +85,7 @@ docs/
| waf | [../waf/README.md](../waf/README.md) | WAF 网关(Go, coraza) | | waf | [../waf/README.md](../waf/README.md) | WAF 网关(Go, coraza) |
| gateway | [../gateway/README.md](../gateway/README.md) | 应用层网关(C++20) | | gateway | [../gateway/README.md](../gateway/README.md) | 应用层网关(C++20) |
| db | [../db/README.md](../db/README.md) | 通用数据库服务(pymsg) | | db | [../db/README.md](../db/README.md) | 通用数据库服务(pymsg) |
| db2 | [../db2/README.md](../db2/README.md) | C++20 数据库 RPC 服务(db 重实现) |
| confs | [../confs/README.md](../confs/README.md) | 统一配置服务 | | confs | [../confs/README.md](../confs/README.md) | 统一配置服务 |
| static | [../static/README.md](../static/README.md) | 前端托管服务 | | static | [../static/README.md](../static/README.md) | 前端托管服务 |
| dnfs | [../dnfs/README.md](../dnfs/README.md) | 树状网络文件系统 | | dnfs | [../dnfs/README.md](../dnfs/README.md) | 树状网络文件系统 |
@@ -0,0 +1,174 @@
# db2 模块安全审计报告
**审计日期**: 2026-08-06
**审计范围**: `db2/src/db2.cpp`(全部实现)、`Makefile`、`docker-entrypoint.sh`
**对照基准**: Python 版 `db/dbv2.py`(接口兼容性对比)
**代码版本**: main 分支 fad749f
---
## 修复进度
| 优先级 | 总数 | 已修复 | 未修复 |
|--------|------|--------|--------|
| P0 | 2 | 2 | 0 |
| P1 | 1 | 1 | 0 |
| P2 | 2 | 0 | 2 |
| P3 | 4 | 1 | 3 |
| **合计** | **9** | **4** | **5** |
---
## P0 — 严重问题
### 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)
- **当前状态**: ✅ 已修复(2026-08-06)
- **修复**: 新增 `_validate_col` 静态方法,对所有列名(where keys、columns、order_by、INSERT/UPDATE values keys)校验其存在于表 schema 的 `col_map` 中,不存在则抛 `"unknown column"` 错误。5 个注入点全部修复。
`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` 中(不存在则抛错)。最稳妥的做法:两者都做。
---
### P0-2: 空 `in` 数组 → 无 WHERE 子句 → 全表删除/更新
- **位置**: `db2.cpp` L439
- **当前状态**: ✅ 已修复(2026-08-06)
- **修复**: 空 `in` 数组现在生成恒假条件 `parts.push_back("0")`,使 WHERE 子句匹配零行,与 Python 版 SQLAlchemy `column.in_([])` 行为一致。
---
## P1 — 高危问题
### P1-1: 所有值绑定为 TEXT,类型信息丢失
- **位置**: `db2.cpp` L368-392(`_coerce_value` / `_bind_value`)、L476-480(`_bind_params`)
- **当前状态**: ✅ 已修复(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<json::value>`,删除原来的 `is_null` 向量;`_bind_params` 统一调用 `_bind_value`。
修复后 INTEGER/REAL 等数值类型以原生 SQLite 存储类写入,避免 TEXT 列上的字符串比较问题,行为与 Python 版一致。
---
## P2 — 中危问题
### P2-1: 清理时间戳格式不一致(T vs 空格)导致保留期偏移约 1 天
- **位置**: `db2.cpp` L54-60(`utc_now`)、L787-789(`_do_cleanup`)
- **当前状态**: ❌ 未修复
- **详情**:
存储的时间戳用 `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++ 端算截止时间后参数绑定。
---
### P2-2: 缺少配置完整性校验
- **位置**: `db2.cpp` L131-185(`load_config`)
- **当前状态**: ❌ 未修复
- **详情**:
`load_config` 对缺失配置项直接用内置默认值兜底(`get(sec, key, cfg.xxx)` 模式)。Python 版(`dbv2.py` L118-129)校验所有必需配置项,缺失则退出。
后果:confs 提供的 `db.ini` 拥缺字段时 db2 不会报错,可能用错误的默认值静默运行(如 `db_url` 缺失则用 `sqlite:///database.db`)。
- **与 Python 版差异**: Python 版严格校验,C++ 版宽松兜底。
- **修复建议**: 对 `[server] host/port/workers`、`[database] url`、`[logs]` 三项做必填校验,缺失则 `exit(1)`。
---
## P3 — 低危问题
| # | 问题 | 位置 | 说明 |
|---|------|------|------|
| P3-1 | schema 缓存永不失效 | L316-351 | 运行时 `ALTER TABLE ADD COLUMN` 后缓存过期,可能导致查询/写入用错误列信息。两版均存在,实际影响低(运行时改表罕见) |
| P3-2 | `_coerce_value` 对 null 返回 `""`(注释误导) | L368-380 | ✅ 已修复(2026-08-06):`_coerce_value` 改为返回 `json::value`,null 保持为 null,不再返回空字符串;`is_null` 向量已删除 |
| P3-3 | `limit`/`offset` 未校验负值 | L548-551 | `as_int64()` 可为负,SQLite 对负 LIMIT 返回全表。非安全问题但行为可能意外 |
| P3-4 | WHERE 条件数 / IN 数组大小无上限 | — | 恶意客户端可发送数千 WHERE 条件或百万 IN 元素耗尽资源。Python 版同样存在(既有问题) |
---
## 已有安全机制(正确实现)
1. **表名校验** — `db2.cpp` L493-496:仅允许 `[A-Za-z0-9_]`,防止表名级 SQL 注入
2. **值参数化绑定** — `_build_where`/`_do_insert`/`_do_update` 中所有 value 通过 `_bind_value` 按类型绑定,无值级 SQL 注入
3. **per-thread 连接** — L308-312:`thread_local` 连接,避免跨线程共享 prepared statement
4. **异常脱敏** — `RpcServer::_sanitize_error`(libmsg):正则替换绝对路径为 `<redacted>`
5. **仅操作已有表** — `PRAGMA table_info` 反射,无法建表/删表
6. **SQLite PRAGMA 优化** — WAL + synchronous=NORMAL + foreign_keys=ON + busy_timeout=5000 + mmap_size=256MB,与 Python 版一致
7. **schema 缓存加锁** — `_schema_mtx` 保护 `_schema_cache`,线程安全
8. **confs 引导重试** — `retry_on_refused=true`,不要求启动顺序
9. **日志内部直写** — `_log_internal` 绕过 RPC 防递归,异常静默不阻塞业务
10. **后台清理线程** — 条件变量优雅停止,按时间+条数双策略清理
---
## 修复路线图
### 立即(24小时)
- [x] **P0-1**: 列名白名单校验(校验列名在 `col_map` 中存在 + `[A-Za-z0-9_]` 格式校验)
- [x] **P0-2**: 空 `in` 生成恒假条件
### 短期(72小时)
- [x] **P1-1**: 按 JSON 类型分别使用 `sqlite3_bind_int64` / `sqlite3_bind_double` / `sqlite3_bind_text`
- [ ] **P2-2**: 配置完整性校验(必需字段缺失则退出)
### 中期
- [ ] **P2-1**: 清理时间戳格式统一为 T 分隔
- [ ] **P3-3**: limit/offset 负值校验
- [ ] **P3-4**: WHERE 条件数 / IN 数组大小上限
---
## 审计方法
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` 数组代码路径确认全表删除行为
@@ -0,0 +1,218 @@
# libsqlite3 模块安全审计报告
**审计日期**: 2026-08-06
**审计范围**: `libsqlite3/inc/us/Sqlite3.hpp`、`Makefile`、`_set.sh`、`README.md`
**代码版本**: main 分支
---
## 修复进度
| 优先级 | 总数 | 已修复 | 未修复 |
|--------|------|--------|--------|
| P0 | 3 | 3 | 0 |
| P1 | 4 | 4 | 0 |
| P2 | 6 | 4 | 2 |
| **合计** | **13** | **11** | **2** |
---
## 模块现状
`libsqlite3` 当前实际是一个**单头文件 C++ RAII 封装库**,只提供 `qing::Sqlite3`(数据库连接)和 `qing::Sqlite3_stmt`(预编译语句)。
- 整个模块**没有任何单元测试或示例**
- `Makefile` 已经**完全注释掉编译规则**,只保留 `install/uninstall`
- `_set.sh` 只调用 `make install`,不编译任何产物
- 全项目**唯一引用方**是 `gateway/src/main.cpp`(`#include <us/Sqlite3.hpp>`),但对应的使用代码已被注释掉
- `db2` 直接使用原生 `sqlite3.h` API,**没有使用本封装**
结论:**该模块目前处于事实上的废弃/未使用状态**,但头文件仍被 `_inst.sh` 安装到系统目录,存在被误用的风险。
---
## P0 — 严重问题
### P0-1: 未定义拷贝/移动语义,存在 double free / use-after-free
- **位置**: `Sqlite3.hpp` L7-60(`Sqlite3_stmt`)、L62-97(`Sqlite3`)
- **当前状态**: ✅ 已修复(2026-08-06)
- **修复**: 为 `Sqlite3` 和 `Sqlite3_stmt` 显式 `= delete` 拷贝构造和拷贝赋值,并实现 `noexcept` 移动构造/移动赋值,移动后把源对象指针置 `nullptr`。析构函数先判空再释放,彻底消除 double free 和 use-after-free 风险。
修复前代码问题:
```cpp
Sqlite3_stmt stmt(std::string& sql) { // L91
return Sqlite3_stmt(db, sql.c_str()); // L92
}
```
`return` 依赖 NRVO/copy elision。若编译器未成功省略,会调用拷贝构造,触发 double free。即使现代编译器通常能省略,代码仍**不是良构且安全的**。
同理,`Sqlite3` 类也持有 `sqlite3* db` 原始指针,未禁用拷贝/移动,也存在 double free 风险。
---
### P0-2: `Sqlite3::~Sqlite3()` 忽略 `sqlite3_close` 返回值
- **位置**: `Sqlite3.hpp` L74-L76
- **当前状态**: ✅ 已修复(2026-08-06)
- **修复**: `~Sqlite3()` 改用 `sqlite3_close_v2(db)`,它会在所有 prepared statements 释放后才真正关闭数据库,避免 `SQLITE_BUSY` 导致的数据库未关闭问题。同时析构前判空,防止重复关闭。
---
### P0-3: `Sqlite3::exec` 使用字符串 SQL,无参数化绑定接口
- **位置**: `Sqlite3.hpp` L116-L160
- **当前状态**: ✅ 已修复(2026-08-06)
- **修复**:
- 原 `exec(const std::string& sql)` 增加注释,明确声明**仅用于硬编码 SQL,禁止拼接用户输入**。
- 新增模板参数化版本 `exec(const std::string& sql, Args&&... args)`,内部创建 `Sqlite3_stmt` 并按位置绑定参数,彻底避免 SQL 注入。
- 支持绑定类型:`int` / `int64_t` / `double` / `std::string` / `const char*` / `nullptr` / `std::vector<unsigned char>`。
---
## P1 — 高危问题
### P1-1: `stmt()` 参数应为 `const std::string&`
- **位置**: `Sqlite3.hpp` L91
- **当前状态**: ✅ 已修复(2026-08-06)
- **修复**: `stmt()` 参数改为 `const std::string& sql`,现在可以传入字符串字面量和临时对象:
```cpp
auto s = db.stmt("SELECT 1"); // 现在可以编译
```
---
### P1-2: `bind_int` 只支持 32 位 int
- **位置**: `Sqlite3.hpp` L40-L58
- **当前状态**: ✅ 已修复(2026-08-06)
- **修复**: 新增 `bind_int64(int idx, int64_t data)` 使用 `sqlite3_bind_int64`,参数化 `exec` 模板对 `int64_t` / `long` / `long long` 自动路由到该绑定。
---
### P1-3: 缺少 `bind_null`、`bind_double`、`bind_blob` 等接口
- **位置**: `Sqlite3.hpp` L40-L86
- **当前状态**: ✅ 已修复(2026-08-06)
- **修复**: 补充了 `bind_int64`、`bind_double`、`bind_text`、`bind_blob`、`bind_null`,参数化 `exec` 模板自动根据类型分发到对应绑定函数。
---
### P1-4: 异常信息丢失 SQLite 错误详情
- **位置**: `Sqlite3.hpp` L9-L15、L84-L90
- **当前状态**: ✅ 已修复(2026-08-06)
- **修复**: 构造/打开数据库的异常现在把 `sqlite3_errmsg()` 内容拼入 `std::runtime_error`;绑定失败也保留了具体错误描述,便于调用者定位问题。
---
## P2 — 中危问题
### P2-1: `Sqlite3_stmt` 成员 `stmt` 未显式初始化
- **位置**: `Sqlite3.hpp` L58-L59
- **当前状态**: ✅ 已修复(2026-08-06)
- **修复**: `Sqlite3` 和 `Sqlite3_stmt` 的成员指针均显式初始化为 `nullptr`,配合移动语义中的置空操作,避免野指针和重复释放。
---
### P2-2: `read_text` 返回指针生命周期未文档化
- **位置**: `Sqlite3.hpp` L103-L107
- **当前状态**: ✅ 已修复(2026-08-06)
- **修复**: 在 `read_text` 上方增加注释,明确说明返回指针指向 statement 内部缓冲区,statement finalize 或 step 后可能失效,长期保存需复制到 `std::string`。
---
### P2-3: 缺少类型安全读取接口
- **位置**: `Sqlite3.hpp` L96-L107
- **当前状态**: ✅ 已修复(2026-08-06)
- **修复**: 补充了 `read_int64`、`read_double`,保留 `read_int` 和 `read_text`。`column_type` / `column_count` 仍未暴露,如需可继续补充。
---
### P2-4: `exec` 中 `errMsg` 未初始化且存在危险注释
- **位置**: `Sqlite3.hpp` L78-L85
- **当前状态**: ✅ 已修复(2026-08-06)
- **修复**: `errMsg` 显式初始化为 `nullptr`,并删除危险注释 `//sqlite3_close(db)`,避免未来被误恢复导致 SQL 执行错误时关闭数据库连接。
---
### P2-5: `Makefile` install 目标文案错误
- **位置**: `libsqlite3/Makefile` L27
- **当前状态**: ❌ 未修复
- **详情**:
```makefile
install:
@echo "install UniServe_libth..."
```
文案是 `libth`,明显是从 `libth/Makefile` 复制过来的,应改为 `UniServe_libsqlite3`。
- **修复建议**: 修正 echo 文本。
---
### P2-6: `Makefile` 与 `_set.sh` 完全无构建/测试步骤
- **位置**: `libsqlite3/Makefile`、`libsqlite3/_set.sh`
- **当前状态**: ❌ 未修复
- **详情**:
- 全部编译规则被注释
- `_set.sh` 只执行 `make install`
- 头文件库不编译 `.so` 尚可理解,但连编译时语法检查都没有
- **修复建议**: 至少保留一个 `check` 或 `test` 目标,编译一个最小测试文件验证头文件能正常包含和实例化。
---
## 已有安全机制(正确实现)
1. **RAII 思路正确** — 用析构函数自动 `sqlite3_finalize` / `sqlite3_close`
2. **`SQLITE_TRANSIENT` 使用正确** — `bind_text` 传 `-1` 和 `SQLITE_TRANSIENT`,让 SQLite 复制数据,避免悬垂指针
3. **`step()` 返回值语义清晰** — 返回 `bool` 表示是否还有数据行
4. **异常路径不吞异常** — 构造/绑定失败直接抛异常,不返回错误码让调用者忽略
5. **头文件仅暴露最小接口** — 没有暴露原始 `sqlite3*`/`sqlite3_stmt*`,封装完整
---
## 修复路线图
### 立即(24小时)
- [x] **P0-1**: 为 `Sqlite3` 和 `Sqlite3_stmt` 显式 delete 拷贝、实现移动构造/赋值
- [x] **P0-2**: `~Sqlite3()` 改用 `sqlite3_close_v2` 或检查返回值
- [x] **P0-3**: 为 `exec` 增加参数化绑定,限制字符串版本仅用于硬编码 SQL
### 短期(72小时)
- [x] **P1-1**: `stmt()` 参数改为 `const std::string&`
- [x] **P1-2**: 增加 `bind_int64`
- [x] **P1-3**: 补充 `bind_null`、`bind_double`、`bind_blob`
- [x] **P1-4**: 异常信息携带 `sqlite3_errmsg` 和错误码
### 中期
- [x] **P2-1**: 成员指针初始化为 `nullptr`
- [x] **P2-2**: `read_text` 生命周期文档化
- [x] **P2-3**: 补充读取接口(int64/double)
- [x] **P2-4**: 初始化 `errMsg` 并删除危险注释
- [ ] **P2-5**: 修正 `Makefile` echo 文案
- [ ] **P2-6**: 增加最小编译检查目标
---
## 审计方法
1. 通读 `libsqlite3/inc/us/Sqlite3.hpp` 全部 98 行实现
2. 审查 `libsqlite3/Makefile`、`_set.sh`、`README.md` 的安装与构建流程
3. 使用 `grep` 全项目查找 `Sqlite3.hpp`、`Sqlite3`、`Sqlite3_stmt`、`libus_sqlite3` 的引用情况
4. 对照 C++ 资源管理规则检查拷贝/移动/析构语义
5. 对照 SQLite3 C API 文档检查 API 使用正确性(绑定、关闭、finalize)
6. 评估接口完整性(参数化绑定、类型绑定、读取接口)
+135 -14
View File
@@ -1,6 +1,10 @@
#include <cstdint>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <sqlite3.h> #include <sqlite3.h>
#include <string>
#include <type_traits>
#include <vector>
namespace qing { namespace qing {
@@ -9,31 +13,77 @@ namespace qing {
Sqlite3_stmt(sqlite3 *db, const char *sql) { Sqlite3_stmt(sqlite3 *db, const char *sql) {
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr); int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
if (rc != SQLITE_OK) { if (rc != SQLITE_OK) {
std::cerr << "Failed to prepare sql stmt: " << sqlite3_errmsg(db) << std::endl; std::string err = sqlite3_errmsg(db);
throw std::runtime_error("Failed to prepare sql stmt"); std::cerr << "Failed to prepare sql stmt: " << err << std::endl;
throw std::runtime_error("Failed to prepare sql stmt: " + err);
} }
} }
~Sqlite3_stmt() { ~Sqlite3_stmt() {
sqlite3_finalize(stmt); if (stmt) {
sqlite3_finalize(stmt);
stmt = nullptr;
}
}
Sqlite3_stmt(const Sqlite3_stmt&) = delete;
Sqlite3_stmt& operator=(const Sqlite3_stmt&) = delete;
Sqlite3_stmt(Sqlite3_stmt&& other) noexcept : stmt(other.stmt) {
other.stmt = nullptr;
}
Sqlite3_stmt& operator=(Sqlite3_stmt&& other) noexcept {
if (this != &other) {
if (stmt) sqlite3_finalize(stmt);
stmt = other.stmt;
other.stmt = nullptr;
}
return *this;
} }
void bind_int(int idx, int data) { void bind_int(int idx, int data) {
int rc = sqlite3_bind_int(stmt, idx, data); int rc = sqlite3_bind_int(stmt, idx, data);
if (rc != SQLITE_OK) { if (rc != SQLITE_OK) {
std::cerr << "Failed to bind an integer to sql stmt." << std::endl;
throw std::runtime_error("Failed to bind an integer to sql stmt"); throw std::runtime_error("Failed to bind an integer to sql stmt");
} }
} }
void bind_int64(int idx, int64_t data) {
int rc = sqlite3_bind_int64(stmt, idx, data);
if (rc != SQLITE_OK) {
throw std::runtime_error("Failed to bind an int64 to sql stmt");
}
}
void bind_double(int idx, double data) {
int rc = sqlite3_bind_double(stmt, idx, data);
if (rc != SQLITE_OK) {
throw std::runtime_error("Failed to bind a double to sql stmt");
}
}
void bind_text(int idx, const std::string &data) { void bind_text(int idx, const std::string &data) {
int rc = sqlite3_bind_text(stmt, idx, data.c_str(), -1, SQLITE_TRANSIENT); int rc = sqlite3_bind_text(stmt, idx, data.c_str(), -1, SQLITE_TRANSIENT);
if (rc != SQLITE_OK) { if (rc != SQLITE_OK) {
std::cerr << "Failed to bind a text to sql stmt." << std::endl;
throw std::runtime_error("Failed to bind a text to sql stmt"); throw std::runtime_error("Failed to bind a text to sql stmt");
} }
} }
void bind_blob(int idx, const std::vector<unsigned char>& data) {
int rc = sqlite3_bind_blob(stmt, idx, data.data(), static_cast<int>(data.size()), SQLITE_TRANSIENT);
if (rc != SQLITE_OK) {
throw std::runtime_error("Failed to bind a blob to sql stmt");
}
}
void bind_null(int idx) {
int rc = sqlite3_bind_null(stmt, idx);
if (rc != SQLITE_OK) {
throw std::runtime_error("Failed to bind null to sql stmt");
}
}
bool step() { bool step() {
int rc = sqlite3_step(stmt); int rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW && rc != SQLITE_DONE) { if (rc != SQLITE_ROW && rc != SQLITE_DONE) {
@@ -42,6 +92,15 @@ namespace qing {
return rc == SQLITE_ROW && rc != SQLITE_DONE; return rc == SQLITE_ROW && rc != SQLITE_DONE;
} }
// 执行无结果集语句(INSERT / UPDATE / DELETE / DDL),返回影响行数
int execute() {
int rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
throw std::runtime_error("Failed to execute statement");
}
return sqlite3_changes(sqlite3_db_handle(stmt));
}
/* /*
* int len = sqlite3_column_bytes(stmt, 1); // 获取字节长度(不含结束符) * int len = sqlite3_column_bytes(stmt, 1); // 获取字节长度(不含结束符)
* std::string name(reinterpret_cast<const char*>(rawName), len); * std::string name(reinterpret_cast<const char*>(rawName), len);
@@ -50,13 +109,22 @@ namespace qing {
return sqlite3_column_int(stmt, idx); return sqlite3_column_int(stmt, idx);
} }
int64_t read_int64(int idx) {
return sqlite3_column_int64(stmt, idx);
}
double read_double(int idx) {
return sqlite3_column_double(stmt, idx);
}
// 返回指向 statement 内部缓冲区的指针,生命周期与当前 statement 绑定。
// statement finalize 或 step 后指针可能失效;如需长期保存请复制到 std::string。
const unsigned char *read_text(int idx) { const unsigned char *read_text(int idx) {
return sqlite3_column_text(stmt, idx); return sqlite3_column_text(stmt, idx);
} }
private: private:
sqlite3_stmt* stmt; sqlite3_stmt* stmt = nullptr;
}; };
class Sqlite3 { class Sqlite3 {
@@ -65,34 +133,87 @@ namespace qing {
Sqlite3(const std::string& filename) { Sqlite3(const std::string& filename) {
int rc = sqlite3_open(filename.c_str(), &db); int rc = sqlite3_open(filename.c_str(), &db);
if (rc != SQLITE_OK) { if (rc != SQLITE_OK) {
std::cerr << "Failed to open database: " << sqlite3_errmsg(db) << std::endl; std::string err = sqlite3_errmsg(db);
std::cerr << "Failed to open database: " << err << std::endl;
sqlite3_close(db); sqlite3_close(db);
throw std::runtime_error("Failed to open sqlite3 database."); throw std::runtime_error("Failed to open sqlite3 database: " + err);
} }
} }
~Sqlite3() { ~Sqlite3() {
sqlite3_close(db); if (db) {
sqlite3_close_v2(db);
db = nullptr;
}
} }
Sqlite3(const Sqlite3&) = delete;
Sqlite3& operator=(const Sqlite3&) = delete;
Sqlite3(Sqlite3&& other) noexcept : db(other.db) {
other.db = nullptr;
}
Sqlite3& operator=(Sqlite3&& other) noexcept {
if (this != &other) {
if (db) sqlite3_close_v2(db);
db = other.db;
other.db = nullptr;
}
return *this;
}
// 仅用于硬编码 SQL;禁止拼接用户输入!
bool exec(const std::string& sql) { bool exec(const std::string& sql) {
char *errMsg; char *errMsg = nullptr;
int rc = sqlite3_exec(db, sql.c_str(), 0, 0, &errMsg); int rc = sqlite3_exec(db, sql.c_str(), 0, 0, &errMsg);
if (rc != SQLITE_OK) { if (rc != SQLITE_OK) {
std::cerr << "Failed to execute at sqlite3: " << errMsg << std::endl; std::string err = errMsg ? errMsg : "unknown error";
std::cerr << "Failed to execute at sqlite3: " << err << std::endl;
sqlite3_free(errMsg); sqlite3_free(errMsg);
//sqlite3_close(db);
return false; return false;
} }
return true; return true;
} }
Sqlite3_stmt stmt(std::string& sql) { // 参数化执行 INSERT / UPDATE / DELETE / DDL,支持 int / int64_t / double /
// std::string / const char* / nullptr / std::vector<unsigned char>
template<typename... Args>
int exec(const std::string& sql, Args&&... args) {
Sqlite3_stmt s(db, sql.c_str());
int idx = 1;
(_bind_one(s, idx++, std::forward<Args>(args)), ...);
return s.execute();
}
Sqlite3_stmt stmt(const std::string& sql) {
return Sqlite3_stmt(db, sql.c_str()); return Sqlite3_stmt(db, sql.c_str());
} }
private: private:
sqlite3* db; sqlite3* db = nullptr;
template<typename T>
static void _bind_one(Sqlite3_stmt& s, int idx, T&& val) {
using U = std::decay_t<T>;
if constexpr (std::is_same_v<U, std::nullptr_t>) {
s.bind_null(idx);
} else if constexpr (std::is_same_v<U, int> || std::is_same_v<U, short> || std::is_same_v<U, bool>) {
s.bind_int(idx, static_cast<int>(val));
} else if constexpr (std::is_same_v<U, int64_t> || std::is_same_v<U, long> || std::is_same_v<U, long long>) {
s.bind_int64(idx, static_cast<int64_t>(val));
} else if constexpr (std::is_same_v<U, double> || std::is_same_v<U, float>) {
s.bind_double(idx, static_cast<double>(val));
} else if constexpr (std::is_same_v<U, std::string>) {
s.bind_text(idx, val);
} else if constexpr (std::is_same_v<U, const char*>) {
s.bind_text(idx, std::string(val));
} else if constexpr (std::is_same_v<U, std::vector<unsigned char>>) {
s.bind_blob(idx, val);
} else {
static_assert(!std::is_same_v<U, U>, "unsupported bind type");
}
}
}; };
} }