PreparedStatement 防 SQL 注入的核心是用 ? 占位符+setXxx() 传参,杜绝字符串拼接;表名/列名等结构动态项须白名单校验;MyBatis ${} 和 JPA nativeQuery 拼接同理危险,需全面排查修复。

用 PreparedStatement 替换拼接字符串的 Statement
SQL 注入的本质是用户输入被当成了 SQL 语法的一部分执行。老代码里常见 String sql = "SELECT * FROM users WHERE name = '" + name + "'" 这种写法,只要 name 是用户可控的(比如表单、URL 参数),攻击者输个 ' OR '1'='1 就能绕过条件。
修复核心就一条:所有动态值必须走参数占位符,不能字符串拼接。
- 把
Statement和executeQuery(sql)全部换成PreparedStatement,用?占位 - 用
setString()、setLong()等方法传值,JDBC 驱动会自动转义 - 注意:
setString(1, userInput)的索引从 1 开始,不是 0 - 表名、列名、
ORDER BY字段不能参数化——它们属于 SQL 结构,得靠白名单校验或硬编码
识别并清理已存在的拼接逻辑(尤其是 MyBatis ${})
MyBatis 里 #{} 是安全的(底层用 PreparedStatement),但 ${} 是直接字符串替换,和手拼 SQL 一样危险。legacy 项目里常有 ORDER BY ${sortBy} 或 WHERE status IN (${statusList}) 这类写法。
搜索整个代码库:\$\{[a-zA-Z0-9_]+\},逐个确认是否真需要动态结构。
- 排序字段:改用白名单,比如
if (!Arrays.asList("name", "created_at").contains(sortBy)) throw new IllegalArgumentException(); - IN 查询:用
foreach标签生成多个?占位符,别用${}拼成(?, ?, ?) - 动态表名:提取为配置项或枚举,禁止从 request 取值后直接拼进 SQL
避免 ORM 层的“假安全”——比如 JPA @Query(nativeQuery = true)
JPA 的 @Query 默认用 HQL,参数是安全的;但一旦加了 nativeQuery = true,就退化成原生 SQL,:param 占位符虽能防注入,可如果写成 "SELECT * FROM " + tableName + " WHERE id = :id",表名还是拼的。
这类代码在 IDE 里不报错,运行也正常,但漏洞藏得深。
- 全局搜
@Query.*nativeQuery = true,检查 SQL 字符串里是否有任何字符串拼接 - 禁止在 native query 中使用
+、StringBuilder、String.format拼接表名/列名 - 如果真要动态表名,改用
EntityManager.createNativeQuery()+ 手动白名单校验,比注解更可控
测试时别只测“功能正确”,重点验证边界输入
重构完跑一遍原有单元测试,大概率全绿——因为注入漏洞本来就不影响正常流程。真正要抓的是那些“不该成功却成功了”的 case。
- 对每个带参数的查询接口,手动发请求:在参数里塞
' OR 1=1 --、"); DROP TABLE users; -- - 观察响应是否返回了本不该看到的数据(比如登录接口传
admin'--登录成功) - 数据库日志里查实际执行的 SQL,确认没有出现未转义的单引号或分号
- 注意:有些框架(如 Spring Security)会拦截异常响应,导致错误被吞掉,得看 DB 日志或开启 JDBC 的
logSlowQueries
最麻烦的不是改几行代码,而是 legacy 里那些“看起来像参数、其实是拼接”的隐藏逻辑——比如某个工具类把用户输入先做了一次 replaceAll("'", "''") 就当防注入了,这种伪防护比没防护还危险。










