PreparedStatement能防SQL注入,因其将SQL模板与参数物理隔离:数据库先编译含?的语句,再将用户输入作为纯数据绑定,不参与语法解析;而字符串拼接会直接将输入喂给SQL解析器,极易被绕过。

预编译语句(PreparedStatement)是防止 SQL 注入最可靠、最通用的手段——只要参数值全部走占位符绑定,数据库层面就不可能把用户输入当 SQL 代码执行。
为什么 PreparedStatement 能防注入,而字符串拼接不能
根本区别在于:SQL 结构和参数在数据库中是否被物理隔离。
-
PreparedStatement先把"SELECT * FROM users WHERE id = ?"这类带?的模板发给 MySQL,服务端立即编译成执行计划,此时语法树已固定 - 后续调用
setInt(1, userInput)时,MySQL 只把userInput当作纯数据塞进绑定位置,不参与任何解析或拼接 - 字符串拼接(如
"SELECT * FROM users WHERE name = '" + name + "'")等于把用户输入直接喂给 SQL 解析器,哪怕加了单引号、转义函数,也挡不住宽字节、十六进制绕过等手法
各语言里 PreparedStatement 的关键写法差异
不同语言底层都依赖 JDBC 或原生协议,但占位符和绑定方式容易出错:
- Java:
?占位,必须用ps.setString(1, value)等类型明确的方法,ps.setObject(1, value)不推荐(可能触发驱动反序列化漏洞) - Python(
pymysql):%s是唯一合法占位符,不是 Python 字符串格式化;不要写成'%s'或"%s"加引号,也不要因参数是 int 就换%d - PHP(
PDO):?或命名占位符:name都可,但必须用$stmt->bindParam()或$stmt->execute([':name' => $value]),不能用mysql_real_escape_string()拼接 - Go(
database/sql):?占位,用stmt.Exec(value)绑定,注意sql.NullString等类型需显式处理空值
哪些地方 PreparedStatement 根本防不住
它只保护「值」,不保护「结构」。以下场景必须额外校验:
- 表名、字段名动态拼接:
"SELECT * FROM " + tableName + " WHERE id = ?"——tableName必须白名单匹配(如in_array($tableName, ['users', 'orders'])) -
ORDER BY后的字段或方向:"ORDER BY " + sortField + " " + sortDir——sortField和sortDir需严格枚举(如['id', 'created_at']和['ASC', 'DESC']) -
IN子句参数个数不确定:"WHERE id IN (?, ?, ?)"要根据实际参数数量动态生成占位符串,但生成逻辑本身不能拼接用户输入 - MyBatis 中误用
${}替代#{}:${tableName}会直接替换、不预编译,等同于字符串拼接
最容易被忽略的一点:预编译安全的前提是「所有用户可控输入都进占位符」。只要漏掉一个拼接点,整条链路就失效。检查时重点盯住 SQL 字符串里有没有出现任何变量名、+、.、f-string 或 ${}。











