{}能拦住' or 1=1这类输入,因为它触发jdbc preparedstatement流程:sql模板(如select * from user where id = ?)先编译,参数通过setstring等方法单独绑定,数据库仅将其视为数据而非语法,即使传入恶意字符串也仅作普通值处理。

为什么#{}能拦住' OR 1=1这类输入
因为#{}不是简单加引号或转义,而是触发 JDBC 的 PreparedStatement 流程:SQL 模板先发给数据库编译(如 SELECT * FROM user WHERE id = ?),参数值再通过 setLong(1, value) 或 setString(1, value) 单独传入。数据库只把输入当「数据」,不解析为语法。
攻击者传 1 OR 1=1 --,最终执行的是:WHERE id = '1 OR 1=1 --'——一条查不到结果的普通查询,而非逻辑篡改。
-
#{id}会自动按类型处理:数值不加引号,字符串加单引号,无需手动拼接 - 若参数声明为
#{id, jdbcType=INTEGER},传入非数字(如字符串)会直接抛NumberFormatException,根本到不了 SQL 执行层 - 对象属性访问(如
#{user.name})同样走 TypeHandler 绑定,安全机制一致
哪些地方必须用#{},不能妥协
所有出现在 SQL 值位置的用户输入,都必须用 #{}。常见场景包括:
-
WHERE条件中的字段值:username = #{username}、status IN #{statusList} -
INSERT和UPDATE的列值:VALUES (#{name}, #{email})、SET name = #{name} - 动态 SQL 标签内部的判断值:
<if test="name != null">AND name = #{name}</if> - 即使参数是空字符串、null 或特殊字符(
'、;、--),#{}仍保持安全
${}不是“少用”,而是“非白名单不可用”
${} 本质是字符串替换,传什么就拼什么。它存在的唯一理由,是 JDBC 不允许在表名、列名、ORDER BY 子句等位置使用 ? 占位符——不是 MyBatis 的设计缺陷,是 SQL 语法限制。
但这也意味着:任何未经硬编码白名单校验的 ${column},和裸写 Statement 没区别。
- 排序字段必须映射为枚举或固定集合:
Set.of("id", "created_at", "score"),前端传sort=score_desc,后端解析成真实列名"score"和方向"DESC" - 表名需双重验证:正则
^[a-zA-Z][a-zA-Z0-9_]{2,31}$+ 查询information_schema.tables确认存在 - 绝对禁止将
HttpServletRequest.getParameter("table")直接塞进${}
日志里看到?就代表安全?不一定
MyBatis 日志显示 Preparing: SELECT * FROM user WHERE name = ?,只能说明用了 #{},不代表绝对安全。
容易被忽略的坑:
- 写了
#{name},但 mapper 方法签名是public List<user> find(@Param("name") Object name)</user>——传入String没问题,传StringBuilder可能绕过 TypeHandler - 自定义
TypeHandler里手动拼接 SQL(如ps.setString(i, "'" + parameter + "'")),等于自己造了个${} - SQL 中混用
#{}和${},比如WHERE ${dynamicCondition} AND status = #{status}——前面一截已失守
真正的防线不在日志形态,而在参数是否全程作为「值」参与绑定,且未在任何环节降级为字符串拼接。











