{}能修复sql注入因其触发preparedstatement预编译,参数仅作数据填充;${}用于动态sql结构(如表名)时须严格白名单校验,而误用#{}于非值位置仍会导致漏洞。

为什么#{}能修复SQL注入
因为#{} 不是字符串拼接,而是触发 JDBC 的 PreparedStatement 预编译机制:SQL 模板先发给数据库编译(如 SELECT * FROM user WHERE name = ?),参数值再单独传入,数据库只把它当数据填进去,不参与语法解析。
哪怕传入 admin' OR 1=1 --,最终执行的也只是:SELECT * FROM user WHERE name = 'admin'' OR 1=1 --' —— 一个带单引号的普通字符串,不可能改变 SQL 结构。
哪些地方必须用#{}而不是${}
${} 是纯文本替换,只要用户能控制其内容,就等于把输入直接塞进 SQL 语法里。以下场景若用了 ${},几乎必然出问题:
-
WHERE条件里的值(如username = ${username})→ 改用username = #{username} -
INSERT或UPDATE的字段值(如SET email = ${email})→ 改用SET email = #{email} - 带通配符的模糊查询(如
LIKE '${keyword}')→ 改用LIKE CONCAT('%', #{keyword}, '%')或配合<bind></bind>
动态列名/表名怎么办:不能用#{},但也不能裸用${}
#{} 无法用于表名、列名、ORDER BY 字段等 SQL 片段——因为 PreparedStatement 不支持对这些位置占位。这时候 ${} 是唯一选择,但必须加白名单校验:
- 在 Java 层做枚举或字符串比对,比如只允许
sortField是"id"、"name"、"created_at" - XML 中用
<choose></choose>+<when></when>显式列出合法值,避免运行时拼接 - 绝对不要把前端传来的任意字符串直接塞进
${sortField},哪怕加了“校验”也得确认是严格白名单
容易被忽略的坑:#{}不是万能护身符
#{} 安全的前提是它真的被用在“值”的位置。这几个常见误用会绕过防护:
-
<bind name="sql" value="${userInput}"></bind> WHERE #{sql}→${userInput}先被替换,再进#{},已晚 -
WHERE name LIKE '%#{keyword}%'→ 单引号是硬编码,#{keyword}被当成字符串字面量,结果变成WHERE name LIKE '%?%',报错或查不到 - MyBatis-Plus 的
QueryWrapper调用eq("name", userInput)安全,但若手写apply("name = " + userInput)就彻底失效
真正关键的不是记住了 #{} ,而是每次写 SQL 时都问一句:这个变量代表的是“数据值”,还是“SQL 结构的一部分”——前者用 #{} ,后者必须隔离+校验,不能偷懒。











