docs(security): 新增 libsqlite3 安全审计报告

- 记录 P0/P1/P2 级别问题与修复状态
- 已修复 double free / 移动语义、close_v2、const 引用等 5 项问题
This commit is contained in:
qing committed 2026-08-06 18:07:14 +00:00
1 parent 985ff47c84
commit 1c55443d43
1 file changed
+245
@@ -0,0 +1,245 @@
# libsqlite3 模块安全审计报告
**审计日期**: 2026-08-06
**审计范围**: `libsqlite3/inc/us/Sqlite3.hpp`、`Makefile`、`_set.sh`、`README.md`
**代码版本**: main 分支
---
## 修复进度
| 优先级 | 总数 | 已修复 | 未修复 |
|--------|------|--------|--------|
| P0 | 3 | 2 | 1 |
| P1 | 4 | 1 | 3 |
| P2 | 5 | 2 | 3 |
| **合计** | **12** | **5** | **7** |
---
## 模块现状
`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` L78-L89
- **当前状态**: ❌ 未修复
- **详情**:
`bool exec(const std::string& sql)` 接收完整 SQL 字符串。虽然函数本身不拼接,但调用者容易把外部输入拼进 SQL,且该函数**没有提供参数化绑定版本**,迫使调用者自行处理转义或只能用于硬编码 SQL。
- **修复建议**: 至少提供文档警告“禁止拼接用户输入”;更推荐提供参数化版本或强制仅用于常量 SQL。
---
## 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` L21-L27
- **当前状态**: ❌ 未修复
- **详情**:
`bind_int` 调用 `sqlite3_bind_int`,只能绑定 32 位整数。SQLite 的 `INTEGER` 可以存 64 位,如果用户要绑定 `int64_t`,会静默截断。
- **修复建议**: 增加 `bind_int64(int idx, int64_t data)` 使用 `sqlite3_bind_int64`。
---
### P1-3: 缺少 `bind_null`、`bind_double`、`bind_blob` 等接口
- **位置**: `Sqlite3.hpp` L21-L35
- **当前状态**: ❌ 未修复
- **详情**:
当前只提供 `bind_int` 和 `bind_text`,缺少 `bind_null`、`bind_blob`、`bind_double`、`bind_int64`。功能不完整,无法满足通用数据库操作。
- **修复建议**: 补充上述接口。
---
### P1-4: 异常信息丢失 SQLite 错误详情
- **位置**: `Sqlite3.hpp` L9-L15、L21-L35、L65-L70
- **当前状态**: ❌ 未修复
- **详情**:
所有异常都使用固定字符串,例如:
```cpp
throw std::runtime_error("Failed to prepare sql stmt");
```
没有把 `sqlite3_errmsg(db)` 或错误码带入异常。调用者无法区分“语法错误”“无此表”“只读数据库”等不同错误。
- **修复建议**: 将 `sqlite3_errmsg(db)` 和错误码拼入异常信息。
---
## P2 — 中危问题
### P2-1: `Sqlite3_stmt` 成员 `stmt` 未显式初始化
- **位置**: `Sqlite3.hpp` L58-L59
- **当前状态**: ✅ 已修复(2026-08-06)
- **修复**: `Sqlite3` 和 `Sqlite3_stmt` 的成员指针均显式初始化为 `nullptr`,配合移动语义中的置空操作,避免野指针和重复释放。
---
### P2-2: `read_text` 返回指针生命周期未文档化
- **位置**: `Sqlite3.hpp` L54-L56
- **当前状态**: ❌ 未修复
- **详情**:
`read_text` 返回 `const unsigned char*`,生命周期依赖于 statement。statement finalize 后指针失效,头文件中没有任何文档说明。
- **修复建议**: 在注释中明确说明返回指针的生命周期;或提供返回 `std::string` 的安全版本。
---
### P2-3: 缺少类型安全读取接口
- **位置**: `Sqlite3.hpp` L49-L56
- **当前状态**: ❌ 未修复
- **详情**:
只有 `read_int` 和 `read_text`,缺少 `read_int64`、`read_double`、`read_blob`、`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` 或检查返回值
- [ ] **P0-3**: 为 `exec` 增加文档警告,限制仅用于硬编码 SQL
### 短期(72小时)
- [x] **P1-1**: `stmt()` 参数改为 `const std::string&`
- [ ] **P1-2**: 增加 `bind_int64`
- [ ] **P1-3**: 补充 `bind_null`、`bind_double`、`bind_blob`
- [ ] **P1-4**: 异常信息携带 `sqlite3_errmsg` 和错误码
### 中期
- [x] **P2-1**: 成员指针初始化为 `nullptr`
- [ ] **P2-2**: `read_text` 生命周期文档化,或提供 `std::string` 版本
- [ ] **P2-3**: 补充读取接口(int64/double/blob/column_type/column_count)
- [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. 评估接口完整性(参数化绑定、类型绑定、读取接口)