SAST 扫 SQL 注入,难点通常不在“能不能扫出来”,而在“扫出来的东西能不能信”。很多团队把代码丢进工具后,要么告警太多无法处理,要么线上出了问题却没在报告里提前看到。
这类问题的根源,往往是对 SAST 的能力边界、规则配置和验证方式理解不够。下面按“为什么能发现、扫描前怎么配、误报漏报怎么处理、结果怎么验证”四个环节梳理,帮助你判断一条 SQLi 告警到底值不值得优先修。
为什么 SAST 能发现 SQL 注入,但不能直接全信
SAST 之所以能识别 SQL 注入,本质上靠的是 AST 分析和污点追踪。工具会把 HTTP 参数、Cookie、路径变量这类用户输入标记为污染源,然后沿着数据流往后看,判断这些值是否在未净化、未参数化的情况下流入数据库执行函数。
例如下面这种最典型的字符串拼接:
String sql = "SELECT * FROM users WHERE id = " + userId;在这类场景里,SAST 往往能比较稳定地命中问题,因为它既能看到字符串拼接,也能识别后续是否调用了数据库执行接口,例如 Statement.execute()、jdbcTemplate.query()。
但这并不意味着扫描报告可以直接等同于漏洞清单。SAST 擅长找“明显的数据流问题”,却不擅长理解所有运行时上下文。像二次注入、动态表名或列名拼接、ORM 中错误使用 ${} 这类场景,经常会漏掉;而某些看起来危险、实际并不会执行 SQL 的代码路径,又可能被误报出来。
因此,正确的理解应该是:SAST 很适合做首轮发现和批量筛查,但不能替代人工判断,更不能替代真实请求下的验证。
扫描前必须确认的 3 个配置点
SQL 注入扫描不是把仓库交给工具就结束了。规则没开、source 没标、safe sink 没排除,最后得到的报告参考价值会很低。上线扫描前,至少要先确认下面三个点。

1. 启用污点分析或数据流分析
如果工具没有真正进入污点分析模式,很多 SQLi 规则只会停留在“关键词匹配”层面,结果要么太浅,要么噪声很多。
常见例子包括:
- SonarQube 中启用对应规则,例如
java:S2077 - CodeQL 中加载
sql-injection.ql查询
这一步的目的,是让工具知道“不只是找 SQL 字符串”,而是要看“用户输入是否流到了执行点”。
2. 明确哪些入口属于用户可控输入
默认情况下,Spring Controller 中的 @RequestParam、@PathVariable 往往会被当作污染源处理。但如果项目用了自定义封装对象来承接请求参数,SAST 不一定能自动识别这些字段的污染属性。
这时就需要在工具配置里显式声明:哪些类、哪些字段属于 source。否则,明明用户可控的数据可能在扫描视角里变成“普通变量”,后续再危险的拼接也未必会报。
3. 确认已知安全调用能被正确排除
SQL 注入检测不能只会报问题,也要能识别哪些写法已经做了参数化处理。像下面这些常见安全调用,原则上不应和字符串拼接一起被混为一谈:
PreparedStatementNamedParameterJdbcTemplate- MyBatis 的
#{}语法
这里还有一个容易忽视的点:规则库版本必须跟得上框架语义。比如旧版 SonarJava 在某些 Hibernate HQL 参数化查询上就可能出现误报。如果规则库过旧,即便代码已经按规范写了,报告也可能把你带偏。
常见误报和漏报,通常出在哪些地方
实际项目里,最让人头疼的往往不是“没有告警”,而是“不知道哪些该信”。下面几类场景最常见,也最值得优先处理。

MyBatis 动态 SQL 容易漏报
典型例子是:
select * from ${tableName}如果工具没有启用动态 SQL 上下文分析,或者 MyBatis 适配能力不足,这类写法很可能直接被跳过。可它在风险上又偏偏很高,因为 ${} 是直接拼接,不做参数化。
更稳妥的处理方式有两种:
- 改成
#{tableName}并增加白名单校验 - 在 SAST 配置中显式启用 MyBatis 插件或相关规则支持
需要注意的是,表名、列名这类对象本身很多时候并不能简单参数化,因此白名单往往比“转义一下”更关键。
日志打印经常制造误报
另一类高频噪声来自日志。例如:
log.info("executing: " + sql)这里的 sql 虽然出现在字符串拼接里,但日志打印本身并不是数据库执行点。部分规则如果对调用链判断不够精确,就可能把这种语句误认为“SQL 被执行”。
遇到这种情况,通常有两个处理方向:
- 在代码里加
// NOSONAR注释,明确标记该处已人工确认 - 在规则配置中忽略日志相关方法调用链
前者适合少量例外,后者适合项目里这类模式很多、长期会反复出现的情况。
框架语义识别不到位也会失真
框架封装越多,SAST 越依赖对框架语义的理解。比如 Spring Data JPA 的 @Query(nativeQuery = true),如果里面还夹带了字符串拼接,部分工具未必能准确识别危险性。
这类问题的治理思路,通常不只是“调规则”,还要尽量统一编码方式。一个常见建议是:能不用 nativeQuery 就不用,优先改成 JPQL 或 Criteria API。这样既减少真实风险,也降低工具识别难度。
扫描结果怎么验证,才能形成闭环
真正有价值的工作,不是统计报表里有多少高危,而是把每条告警放回代码路径里核实。判断一条 SQLi 是否成立,至少要追三件事:输入是否可控、中间是否真的安全、结尾是否落到执行点。

先人工走通输入到执行的链路
对每条告警,都应该反向追溯数据流:
- 源头是不是用户真实可控输入
- 中间有没有所谓“过滤”函数,但实际上防护无效
- 最终是否真的进入了
executeUpdate()、queryForObject()这类执行点
例如 StringEscapeUtils.escapeSql() 这类处理,很多时候并不能真正防住 UNION 注入。如果工具因为看到“有过滤动作”就降低风险级别,人工复核就必须把这一层补回来。
重点盯 WHERE、ORDER BY、GROUP BY 的动态拼接
不少团队会优先关注 WHERE 条件里的拼接,但实际排查时,ORDER BY、GROUP BY 也必须重点看。原因很简单:SAST 对排序字段拼接的检测通常不够强,而这又是盲注和绕过场景里很常见的入口。
例如:
"ORDER BY " + sortField这种写法在业务上看似只是排序字段可配,实际如果没有白名单控制,同样可能演变成注入面。
再用轻量 DAST 做交叉确认
人工看过数据流后,最好再做一次轻量动态验证。一个常见做法,是用 OWASP ZAP 对告警接口发送类似下面的测试载荷:
' OR 1=1 --如果 SAST 报风险,而 ZAP 也能在接口行为上打出异常结果,比如返回了不应出现的数据、过滤逻辑失效或报错信息异常,那么这条问题的优先级就应显著上升。
也就是说,真正闭环的判断方式不是“工具说有”,而是“SAST 指出了代码路径,人工确认了可达性,DAST 证明了风险可以触发”。做到这一步,扫描结果才算从告警变成了可处置的问题。
比调参更重要的,是改掉危险写法
多数团队真正卡住的,从来不是工具不会扫,而是面对告警时不愿意改代码习惯。SAST 报出的每一条 SQL 注入风险,背后通常都对应着一个没有参数化的 Statement,或者一个被滥用的 ${}。
所以,扫描配置当然要做,规则调优也有必要,但最终决定效果的还是代码本身:能否统一使用参数化查询,能否减少动态拼接,能否限制框架里那些高风险、低收益的写法。把这些问题改掉,往往比继续在工具参数里打转更有价值。







