参数化查询只保护“?”占位符位置的值,不保护sql模板本身;当变量用于表名、字段名、order by等非值上下文时,预编译完全失效。

二次SQL注入不是“没转义干净”,而是代码把数据库当成了信任边界——从DB读出来的数据,直接拼进新SQL,等于把脏水倒进刚洗过的杯子。
为什么参数化查询在二次注入场景下会失效
参数化查询只保护 ? 占位符位置的值,但不保护 SQL 模板本身。一旦变量被用于表名、字段名、ORDER BY、GROUP BY 或 WHERE 子句的非值上下文,预编译就完全不生效。
- 典型错误:
String sql = "SELECT * FROM " + tableName + " WHERE id = ?";——tableName是从 DB 查出的,却直接拼接,PreparedStatement对它毫无约束力 - 更隐蔽的坑:
orderByRaw("{$sortField} {$direction}")(MyBatis 的orderByRaw)或 JPA 的@Query("... ORDER BY " + sortField),都绕过参数化 - 字符集切换不影响 Java 的
PreparedStatement安全性,但开发者常误以为“用了 PreparedStatement 就万事大吉”,从而在其他地方松懈校验
Java 中哪些地方必须做白名单校验
所有从数据库读出、又参与 SQL 构造的字符串,只要没走 PreparedStatement 的 ? 占位,就必须在拼接前做语义校验。不能依赖“它之前存进去是安全的”。
Java Maven 项目审查工作流的通用输入处理。当 Java Maven 项目以 ZIP压缩包或 GitLab 仓库 URL 形式提供且需要...时使用。
-
user_id字段:用Long.parseLong(idStr)或Objects.nonNull(id) && id instanceof Long,拒绝任何含字母、空格、符号的输入 -
table_name/column_name:硬编码白名单,如Set.of("users", "orders", "products").contains(tableName) -
sort_field:限定为Set.of("created_at", "updated_at", "name").contains(sortField),禁止传入password或(SELECT ...) -
status类枚举值:必须映射到enum,再通过toString()或name()参与拼接,杜绝任意字符串透传
为什么对 DB 字段做 StringEscapeUtils.escapeSql() 是无效且危险的
这个 Apache Commons 方法早已被标记为 @Deprecated,它只处理单引号、反斜杠等极少数字符,对 UNION、SELECT、EXEC 等关键字零防护;且在数字上下文(如 id = ${value})、正则匹配、或配合宽字节漏洞时极易绕过。
- 常见误用:
String safeName = StringEscapeUtils.escapeSql(nameFromDb); query = "WHERE username = '" + safeName + "'";—— 攻击者仍可提交admin' UNION SELECT ... --,因为escapeSql()不过滤UNION - 真正该做的:在拼接前判断
nameFromDb是否符合用户名正则^[a-zA-Z0-9_\u4e00-\u9fa5]{2,20}$,不符合就抛异常或拒绝执行 - ORM 场景下尤其注意:MyBatis 的
${}是字符串替换,#{}才是预编译;JPA 的@Query若含拼接,必须用CriteriaBuilder或Specification重构
最易被忽略的一点:缓存层。Redis 或本地缓存里存的 username,和数据库里查出来的是同一份脏数据——校验动作必须发生在最终 SQL 构造前,而不是“第一次入库时”。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










