预处理语句能防sql注入,因其将sql模板与数据物理隔离:数据库先编译“select * from users where name = ?”生成固定执行计划,再通过sqlite3_bind_text传入的值仅作字节流填充,不参与语法解析,故“admin' or '1'='1”仍被视作普通字符串。

必须用预处理语句,其他所有手段(转义、过滤、黑名单、正则替换)都是无效补丁。
sqlite3_prepare_v2 为什么能防注入
它把 SQL 模板和数据彻底拆开:数据库先编译 "SELECT * FROM users WHERE name = ?" 这个结构,生成执行计划;之后你调用 sqlite3_bind_text 传入的值,只会被当作文本字节流塞进对应位置,不会触发语法解析。哪怕你传入 "admin' OR '1'='1",最终执行的仍是带一个问号参数的固定查询,' 和 OR 不会被当成 SQL 符号处理。
-
sqlite3_exec直接执行拼接字符串,或手动用std::string::replace替换引号——这两者都无效,因为攻击 payload 可能含 Unicode 绕过、嵌套注释、空字节等变体,靠字符替换永远漏 - 占位符只支持
?(位置)或:name(命名),不支持${var}或%s - 绑定必须在
sqlite3_step前完成,且每个?必须且只能绑定一次 - 如果绑定后修改了原始字符串内存(比如局部
std::string出作用域),sqlite3_bind_text第四个参数设为-1才安全(让 SQLite 自行拷贝)
MySQL 和 SQLite 预处理的关键差异
MySQL 的预处理要求显式声明参数类型,SQLite 则按绑定函数自动推断(sqlite3_bind_int vs sqlite3_bind_text)。这意味着:
- MySQL 中若用
mysql_stmt_bind_param传错类型(比如把字符串当整数绑),可能触发截断或转换异常 - SQLite 更宽松但更易掩盖逻辑错误——比如把时间戳字符串误用
bind_int,值变成0 - 失败时检查
mysql_stmt_errno而非全局mysql_errno -
sqlite3_prepare_v2返回SQLITE_OK表示语法合法,不保证表/列存在——运行时才报错
ODBC 下 SQLBindParameter 为何常失效
SQLBindParameter 本身不防注入,它只是把参数值安全地传给驱动,前提是你的 SQL 语句里得用 ? 占位符。如果写的是 "SELECT * FROM users WHERE name = '" + name + "'" 这种拼字符串的 SQL,哪怕后面调了该函数也完全没用——因为参数根本没进绑定流程。
- 常见错误现象:输入
' OR 1=1 --就返回所有行;或报错[Microsoft][ODBC Driver Manager] Invalid string or buffer length,其实是绑定时类型/长度不匹配,掩盖了真正的注入风险 - 必须用
SELECT * FROM users WHERE name = ?这类带问号的预编译语句,再调SQLPrepare+SQLBindParameter -
ParameterValuePtr指向的内存必须在整个SQLExecute过程中有效(不能是局部std::string的c_str()) - 预编译语句未重用会引发性能与兼容性问题:某些 ODBC 驱动(如 PostgreSQL ODBC)可能静默回退到客户端模拟拼接——等于白防
RAII 和生命周期陷阱最容易被忽略
预处理语句对象(sqlite3_stmt* 或 MYSQ_STMT*)是资源,不是纯数据。没正确清理会导致句柄泄漏,尤其在异常路径下。
- 不要裸指针管理:用 RAII 封装,析构函数里调用
sqlite3_finalize或mysql_stmt_close - 避免跨线程复用同一语句句柄——SQLite 允许,但 MySQL 要求每个线程独立
prepare - 连接关闭前未
finalize语句,SQLite 可能返回SQLITE_BUSY,MySQL 则静默丢弃 - 动态表名或列名无法参数化:
"SELECT * FROM ?"是非法语法,这类场景必须靠白名单校验
最危险的不是不会写预处理,而是写了却在某个分支里悄悄拼接了字符串,或者绑定了参数却忘了检查返回值——防御失效往往发生在你确信“已经防住了”的地方。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











