动态sql注入风险需开发者主动隔离结构与值,表名、列名等元数据必须来自白名单或枚举;#{}仅防护参数值,无法防御拼接的sql结构;in查询应使用namedparameterjdbctemplate;模糊查询禁用${},推荐concat或bind;jpa应避免spel表达式,优先用criteria api。

动态SQL注入风险不会被框架自动拦截,必须靠开发者主动隔离「结构」和「值」——表名、列名、排序字段、条件关键词这些元数据,永远不能来自用户输入。
为什么 #{} 也防不住动态表名和 ORDER BY
MyBatis 的 #{} 只对参数值做 PreparedStatement 绑定,对 SQL 结构本身完全不处理。一旦把用户输入拼进 SQL 字符串里,哪怕只拼一个字段名,防护就形同虚设。
- ❌
"SELECT * FROM " + tableName + " WHERE id = ?"——tableName是运行时拼的,?再安全也救不了整条语句 - ❌
"ORDER BY " + sortField + " " + sortOrder——sortField若是id; DROP TABLE user--,数据库会直接执行 - ✅ 正确做法:所有可变结构必须来自白名单或枚举,比如
Map.of("name", "user_name", "email", "user_email"),用户只传"name",代码查表映射后硬编码进 SQL
IN 查询别硬拼问号,用 NamedParameterJdbcTemplate
原生 JdbcTemplate 不支持动态长度的 IN 列表,强行拼 "WHERE id IN (?, ?, ?)" 会导致参数数量错配或序列化异常。
- ❌ 错误:字符串拼接问号个数:
"WHERE id IN (" + String.join(",", Collections.nCopies(ids.size(), "?")) + ")" - ✅ 正确:交给
NamedParameterJdbcTemplate处理:"WHERE id IN (:ids)",传MapSqlParameterSource("ids", ids) - ⚠️ 注意:
:ids是单个参数名,驱动会自动展开成对应数量的?并绑定,不是字符串替换
MyBatis 模糊查询别用 ${},优先用 CONCAT 或 bind
${} 是纯字符串替换,"%${name}%" 等价于手动拼接,高危。
- ❌
"WHERE name LIKE '%${name}%'"—— 输入admin' OR '1'='1直接触发布尔盲注 - ✅ 推荐一(CONCAT):
"WHERE name LIKE CONCAT('%', #{name}, '%')",#{name}仍走预编译 - ✅ 推荐二(bind):
<bind name="pattern" value="'%' + name + '%'"></bind> WHERE name LIKE #{pattern},变量在 MyBatis 层生成,不暴露给 JDBC
JPA 动态查询别碰 ${} 和 #{#entityName},Criteria API 更可靠
@Query 中混用 SpEL 表达式(如 ${} 或 #{#xxx})等于主动打开注入通道。
- ❌
@Query("SELECT * FROM #{#entityName} WHERE ...")——#entityName是运行时计算的,无法参数化 - ❌
@Query("... LIKE '%${keyword}%'")—— 同样是字符串拼接 - ✅ 替代方案:用
CriteriaBuilder构造查询,字段名、操作符全部来自 Java 类型系统,没有字符串插槽 - ⚠️ 注意:Criteria API 写法略重,但复杂条件 or/and 组合、权限字段过滤等场景下,它比任何字符串模板都可控
真正容易被忽略的是:动态 SQL 的「结构」部分往往藏在工具类、配置项甚至前端传来的 key 名里,而不是明晃晃的 SQL 字符串中。校验入口必须覆盖所有元数据来源,不能只盯住 DAO 层那几行 SQL。











