preparedstatement能防注入,因预编译时仅解析sql结构,执行时参数作为纯数据绑定,不参与语法解析;而字符串拼接使输入混入sql语句,易被篡改为恶意逻辑。

预编译参数化查询能防住绝大多数 SQL 注入,但前提是用对——错一个环节,比如把 ${} 当 #{} 用,或者对 ORDER BY 字段直接拼接,防护就形同虚设。
为什么 PreparedStatement 能防注入,而字符串拼接必出事
根本区别在于:数据库在预编译阶段只解析 SQL 结构(如 SELECT * FROM users WHERE id = ?),问号不参与语法分析;执行时传入的值被当作纯数据处理,不会触发任何语句重写。而拼接方式(如 "WHERE name = '" + input + "'")让输入直接混进 SQL 字符串里,' OR '1'='1 这类输入会变成合法条件,整条语句逻辑就被篡改了。
常见错误包括:
- 以为
addslashes()或mysql_real_escape_string()就安全——字符集不一致时极易绕过 - 用
sprintf()或模板字符串插值代替绑定,比如 PHP 中写"WHERE id = $id" - Java 里用
Statement.execute()而非PreparedStatement,哪怕用了?占位符也无效
mysqli_prepare() 正确用法与三个易漏检查点
PHP 使用原生 MySQLi 时,必须严格走两步:准备语句 + 绑定参数。占位符统一为 ?,类型标识必须匹配(i 整数、s 字符串等),且每一步都要检查返回值。
容易翻车的地方:
-
mysqli_prepare($conn, "SELECT * FROM logs WHERE level = ?")返回false时没判断,后续mysqli_stmt_bind_param()会静默失败 - 绑定类型写错,比如用
s绑定数字 ID,可能导致截断或隐式转换漏洞 - 没设
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT),错误被吞掉,调试时查不到原因
哪些地方不能用 ?,必须白名单或强校验
MySQL 不允许对语法结构成分做参数化:ORDER BY ?、TABLE ?、LIMIT ?, ? 中的第一个参数(offset)都非法。这些部分一旦动态生成,就必须隔离处理。
实操建议:
-
ORDER BY字段:用in_array($sort, ['created_at', 'status'], true)白名单校验,拒绝任何不在列表中的值 -
LIMIT偏移量:先intval(),再检查是否 ≥ 0 且 ≤ 10000(根据业务设上限),不能只靠bind_param('i') - 分表场景的动态表名:正则过滤必须严格,例如
/^[a-z_][a-z0-9_]{1,63}$/i,并禁止匹配系统表前缀(如mysql_)
ORM 框架里的“假安全”陷阱
ORM 并不自动免疫注入。Django 的 raw()、extra() 和原生 cursor.execute() 都不自动参数化;MyBatis 的 ${} 是字符串替换,等价于裸拼接;Laravel 的 DB::select("... $id") 也是高危写法。
关键识别点:
- MyBatis 中只认
#{}安全,${column}必须搭配白名单或硬编码映射 - Django
cursor.execute("SELECT * FROM users WHERE id = %s", [user_id])才合规,漏掉第二个参数就是漏洞 - 所有框架的“原始 SQL”接口,只要带用户输入,就必须手动确认是否走参数绑定路径
最常被忽略的是:动态排序、分页偏移、多租户表名这些“非标准字段”,它们不在常规参数化覆盖范围内,却恰恰是渗透测试最爱打的突破口。











