{} 是预编译占位符,天然防 sql 注入;${} 是字符串拼接,直接拼入 sql 文本,存在严重注入风险;普通查询必须用 #{},仅动态表名、列名、order by 字段等无法参数化场景才可谨慎用 ${},且须经白名单校验。

Spring Security 本身不防 SQL 注入,加再多 SecurityFilterChain 配置也拦不住——它根本不解析请求体、不碰 SQL 构造逻辑。
真正起效的只有三层:输入过滤(可选但易出错)、参数校验(必须)、DAO 层预编译(绝对不可绕过)。
MyBatis 的 #{} 和 ${} 到底怎么用才安全?
#{} 是预编译占位符,由 JDBC 驱动转成 PreparedStatement 参数,SQL 结构和数据分离,天然免疫注入。
${} 是字符串拼接,运行时直接插入 SQL 文本,任何用户输入进去都等于把刀递给攻击者。
- 所有普通字段查询必须用
#{id}、#{username} - 动态表名、列名等无法参数化的场景,必须走白名单校验:
if (!ALLOWED_TABLES.contains(tableName)) throw new IllegalArgumentException(); - 不要用
@Select("SELECT * FROM ${tableName}")这类写法,除非你已确保tableName来自枚举或配置项 -
ORDER BY ${sortField}同理:只允许"created_time"、"status"等固定值,拒绝"id; DROP TABLE user"
为什么自定义 Filter 拦不住 JSON 请求里的 SQL 注入?
因为request.getInputStream() 只能读一次。Spring MVC 的 MappingJackson2HttpMessageConverter 在 Controller 层前就消费了 body,你的 Filter 再读就是空流,甚至抛 IllegalStateException。
- 必须继承
HttpServletRequestWrapper,在构造时用IOUtils.toByteArray(request.getInputStream())缓存原始字节 - 重写
getInputStream()和getReader(),返回缓存副本 - 检测逻辑只对
Content-Type: application/json或application/x-www-form-urlencoded生效,其他类型跳过 - 关键字扫描用正则
(?i)(union|select|insert|drop|xp_),但别指望它 100% 拦住——URL 编码、注释分隔、大小写混用都能绕过
JPA/Hibernate 怎么写 @Query 才不被注入?
命名参数是唯一安全路径;字符串拼接 +String.format 或 + 拼接全是高危操作。
- ✅ 正确:
@Query("SELECT u FROM User u WHERE u.email = :email")+@Param("email") String email - ❌ 错误:
@Query("SELECT u FROM User u WHERE u.email = '" + email + "'") - ❌ 更错:
@Query(value = "SELECT * FROM user WHERE name = ?1", nativeQuery = true)——?1在原生 SQL 中不保证预编译,某些驱动会 fallback 到字符串替换 - 原生 SQL 必须用命名参数:
@Query(value = "SELECT * FROM user WHERE status = :status", nativeQuery = true)
真正容易被忽略的点在于:数据库账号权限。哪怕代码全用 #{},如果应用账号有 DROP TABLE 权限,堆叠注入(如 1'; DROP TABLE user--)仍可能成功。查用户接口只给 SELECT user.*,这是最后一道物理防线。











