{} 安全防注入,因预编译将参数视为纯数据并自动转义;${} 不安全,直接字符串拼接易被单引号注入破坏SQL结构。

占位符让单引号变成数据,而不是语法符号
单引号注入能生效,本质是攻击者输入的 ' 被当作 SQL 字符串边界符解析,从而破坏原有语句结构。比如用户输入 admin' OR '1'='1,拼接后变成:WHERE username = 'admin' OR '1'='1' —— 这里第一个 ' 提前闭合了字符串,后面的内容就被数据库当作新逻辑执行。
而使用占位符(如 #{username} 或 ?)时,数据库驱动(如 MySQL Connector/J)会把整个参数值当作纯数据处理:调用 setString() 方法时,内部自动在值前后加上单引号,并对其中所有单引号做转义(例如 o'reilly → 'o''reilly')。最终执行的语句是:WHERE username = 'admin'' OR ''1''=''1' —— 所有单引号都成了字符串内容,不再具备语法作用。
MyBatis 中 #{} 和 ${} 的行为差异直接决定安全性
这是开发中最容易踩坑的地方:同一个变量,写法不同,安全级别天壤之别。
-
#{username}触发预编译,走PreparedStatement.setString(),参数被隔离为纯值 -
${username}是字符串拼接,MyBatis 直接把变量值原样插入 SQL 模板,等同于手写"WHERE username = '" + username + "'" - 即使你对
${}做了 Java 层过滤,只要漏掉一个字符(比如没处理''或反斜杠),就可能被绕过 - 常见误用场景:动态表名、排序字段(
ORDER BY ${sortBy})、IN子句中的枚举值——这些必须用白名单校验或专用构建器,不能依赖${}
不是所有“带问号的SQL”都真正防注入
占位符防注入的前提是底层确实用了 PreparedStatement,而不是仅在字符串里写了个 ? 却仍用 Statement.execute() 执行。
- JDBC 驱动必须支持预编译,且连接 URL 中未禁用(如 MySQL 的
useServerPrepStmts=false会退化为普通拼接) - 某些 ORM 封装层(尤其是老旧版本)可能对复杂查询降级为字符串拼接,需检查生成日志中是否出现
Preparing: SELECT ... ?类提示 - Spring JDBC 的
JdbcTemplate.query()默认用PreparedStatement,但若手动调用execute(String sql)并传入拼接后的字符串,占位符就失效了
强类型语言也不能完全免责
有人觉得 Java 是强类型,数字型参数天然防注入,这不完全对。
- 如果参数是
String类型(比如 ID 字段虽为数字,但接口接收为字符串),攻击者仍可输入1 OR 1=1,若错误使用${id},照样触发注入 - MyBatis 中
#{id, jdbcType=INTEGER}确实会尝试类型转换,但若传入的是字符串"1 OR 1=1",多数驱动会抛出DataTruncation或SQLException,这算一种被动防护——但前提是没用${}绕过校验 - 真正关键不是语言类型,而是「参数是否与 SQL 结构分离」;哪怕在 Java 里,手写
"WHERE id = " + id依然高危
#{} 和 ${},或者以为加了日志过滤就安全,却没验证到底层执行的是哪条语句。防注入不是加个占位符就万事大吉,得确认它真正在预编译流程里走完了全程。

















