preparedstatement能防注入,因其将sql结构与用户数据在数据库层面彻底隔离:预编译时仅解析含?的模板,执行时参数作为纯值绑定,不参与语法解析,故恶意输入如"1' or '1'='1"仅被视作字符串而非代码。

MySQL预编译语句为什么能防注入
因为SQL结构和用户数据在数据库层面被彻底隔离:预编译阶段只解析模板(如 SELECT * FROM users WHERE id = ?),问号不参与语法分析;执行阶段传入的参数被当作纯值处理,不会触发重解析或改变语义。哪怕输入是 1' OR '1'='1,最终执行的也只是 WHERE id = '1\' OR \'1\'=\'1' —— 一个带单引号的字符串值,不是新逻辑。
各语言中 PreparedStatement 的正确用法
错误写法共性:用字符串拼接构造SQL,哪怕加了转义、过滤、mysql_real_escape_string 都不可靠。
- Java 必须用
Connection.prepareStatement()创建语句,再调用setString()、setInt()等方法赋值;绝不能用Statement.execute("SELECT ... " + input) - PHP 使用
PDO::prepare()或mysqli_prepare(),绑定必须走bindValue()或bind_param();sprintf()、str_replace()拼接一律无效 - Python(MySQLdb / PyMySQL / mysql-connector-python)统一用
%s占位符,参数以元组或字典传入:cursor.execute("SELECT * FROM t WHERE name = %s", (name,));.format()或f-string是高危操作 - Node.js 的
mysql2默认启用参数化,但若显式设options.multipleStatements: true,又拼接用户输入,就会绕过防护
哪些地方不能用 ? 占位符,必须白名单校验
ORDER BY ?、GROUP BY ?、TABLE ?、LIMIT ?, ? 中第一个参数(offset)——这些语法成分无法参数化,数据库会直接报错或忽略占位符。
- 排序字段:只允许
status、created_at、score等固定字段,用if-else或映射表决定最终 SQL,禁止直接插变量 - LIMIT offset:用户输入的 offset 必须先
parseInt()(JS)或int()(Python),再校验 ≥ 0 且 ≤ 合理上限(如 10000),否则可能触发隐式类型转换漏洞 - 动态表名(如分表场景):正则严格过滤,例如
/^[a-z_][a-z0-9_]{1,63}$/i;更稳妥的是查配置中心白名单,匹配不到就拒掉 - MyBatis 中
${}是字符串替换,等同于拼接;只有#{}触发预编译 —— 写成WHERE name = ${name}就等于裸拼
ORM 框架里哪些接口仍可能中招
ORM 不是银弹。它只在标准查询路径(如 Django 的 filter()、Laravel 的 where())下默认参数化;一旦调用底层裸 SQL 接口,防护就失效。
- Django:
raw()、extra()、cursor.execute()都不自动参数化,必须显式传参元组/字典,例如cursor.execute("SELECT * FROM users WHERE id = %s", [user_id]) - Laravel:
DB::select()支持参数数组,但若写成DB::select("SELECT * FROM users WHERE name = '$name'"),就完全暴露 - SQLAlchemy:
text()构造的语句需手动用bindparam(),直接插 f-string 或%格式化即失效
最易被忽略的是日志埋点、导出报表、后台搜索这类“非主干路径”代码——它们常绕过 ORM,又缺乏 Code Review,成了注入温床。











