一、背景
今年在做个AI代码安全审计的项目,代码仓库里十有八九都是Java项目,所以开始研究怎么给Java做代码审计。传统的人工审计,效率低,还容易看花眼。这时候想到了CodeQL。把你代码转换成可查询的数据库,然后用像SQL一样的语法去挖漏洞。
整体思路很简单,就两步:
- 用CodeQL扫描,生成一堆漏洞的报告(JSON格式)。
- 拆解和分析这些报告,判断这个漏洞到底是"确诊"还是"误诊"。
二、操作步骤
2.1 CodeQL 扫描
简单来说,分三步走:
创建数据库:这就好比把Java代码这个"原始食材"加工成CodeQL能处理的"半成品"。
codeql database create my-java-db --language=java --command="mvn compile"my-java-db:给你的数据库起个名儿。--language=java:声明语言是Java。--command:告诉CodeQL你用啥编译项目,比如Maven就用mvn compile,Gradle就用gradle build。这一步是关键,CodeQL会通过编译过程来理解整个代码结构。
运行查询:拿着写好的"问题清单"(QL查询脚本),去数据库里找答案。
codeql database analyze my-java-db codeql/java/ql/src/Security/ --format=sarif-latest --output=results.sarif- 这里我直接用了CodeQL自带的官方安全查询库 (
codeql/java/ql/src/Security/),里面涵盖了SQL注入、SSRF等各种漏洞的检测规则。 - 输出格式我选了
sarif,这是一种标准格式,很多工具都认。当然你也可以输出成CSV或者JSON。
- 这里我直接用了CodeQL自带的官方安全查询库 (
- 查看结果:生成的
results.sarif文件里,就藏着所有疑似漏洞的线索。
2.2 扫描结果解析
这个SARIF/JSON文件,大家刚开始看可能会觉得眼花缭乱。不过我们实际上只要看核心信息:
- 漏洞位置:
location字段会精确告诉你,有问题的代码在哪个文件的第几行第几列。 - 漏洞类型:
ruleId会告诉你它怀疑是啥漏洞,比如java/sql-injection。 - 数据流路径:这是最核心的部分!
codeFlows里会展示数据是怎么从"源头"(Source,比如用户输入)流到"汇点"(Sink,比如执行SQL语句的方法)的。这是我们接下来审计的重点。
三、判断漏洞真假
CodeQL报出来的不一定是真漏洞,它只是个"高度可疑"的警报。我们需要去验证它。
核心思路就四个字:跟踪数据流。具体来说,要检查三个地方:
- 注入点(Source)有效吗? 这个数据真的是来自不可信的用户吗?
- 执行点(Sink)有效吗? 这个函数真的能执行危险操作吗?
- 中间链路干净吗? 数据从源头到执行点的路上,有没有被"洗干净"(过滤、编码)?
我举几个例子来具体讲。
3.1 SQL注入常规案例
CodeQL报告:在 UserController.java 的第35行,可能存在SQL注入。
我们去看看代码:
// UserController.java
public String getUserByName(@RequestParam String name) {
String sql = "SELECT * FROM users WHERE name = '" + name + "'"; // Source: 用户控制的name参数
return jdbcTemplate.queryForObject(sql, String.class); // Sink: 执行SQL查询
}审计过程:
- 源头:
name参数来自用户HTTP请求,完全可控。有效! - 汇点:
jdbcTemplate.queryForObject确实会执行SQL语句。有效! - 链路:数据从
name直接拼接进sql字符串,然后传入汇点。中间没有任何过滤!
结论:这是个真漏洞,实锤的SQL注入。修复方法很简单,用预编译就对了。
3.2 MyBatis审计为例
MyBbatis的情况比较特殊,它的SQL语句很多写在XML文件里。CodeQL同样能扫描出来。
关键点:MyBatis里用 #{} 是预编译,安全的;用 ${} 是字符串拼接,危险的!
CodeQL报告:在 UserMapper.xml 中发现使用 ${}。
<!-- UserMapper.xml -->
<select id="getUser" parameterType="String" resultType="User">
SELECT * FROM users WHERE name = '${name}'
</select>审计过程:
- 源头:
name参数从Java层传入Mapper。 - 汇点:MyBatis框架解析
${name}时,会直接进行字符串替换。 - 链路:直接拼接。
所以只要 name 用户可控,就是真漏洞。CodeQL能识别出这种模式并报警。
四、常见误报案例
CodeQL不是神,很多情况它会"过度紧张"。下面是我遇到的三个典型"假警报",咱们掰开揉碎了分析分析。
4.1 只能控制部分参数
CodeQL报告:在 AdminController.java 的第42行,检测到可能存在SQL注入漏洞,数据流显示用户输入参数直接拼接进SQL语句。
我们来看具体代码:
public String resetPassword(@RequestParam int userId) {
// userId虽然是用户输入,但它被声明为int型
String sql = "UPDATE users SET password='default' WHERE id = " + userId;
jdbcTemplate.update(sql);
return "密码已重置";
}详细审计过程:
- 源头分析:
userId确实是用户通过HTTP请求传入的参数,从来源上看属于"不可信输入",这一点CodeQL判断是对的。 - 汇点分析:
jdbcTemplate.update(sql)方法确实会执行传入的SQL字符串,属于SQL注入的典型危险汇点,CodeQL这里的判断也没问题。 - 关键链路分析:问题出在
userId的数据类型上。它被明确声明为int类型,这意味着什么?当用户在HTTP请求里传入参数时,Spring MVC框架会自动进行类型转换。如果用户想传个带SQL注入的字符串,比如1; DROP TABLE users;--,框架在转换时就会直接报错(类型不匹配),这个恶意字符串根本进不到代码里。最终拼接进SQL的,只能是一个合法的整数。这就从根本上堵死了注入的可能性——攻击者连注入代码的机会都没有,因为参数类型把所有非数字输入都过滤掉了。
所以假漏洞这种情况属于CodeQL在做数据流跟踪时,只关注"数据是否来自用户"以及"是否流向危险操作",但对Java这种强类型语言的类型约束理解不够深入,没意识到基础数据类型本身就能提供一定的安全保障。
4.2 中间链路被清洗
CodeQL报告:在 UserService.java 的第78行,发现用户输入参数未经过滤直接拼接SQL语句,存在SQL注入风险。
代码片段如下:
// UserService.java
public User findUser(String inputName) {
// 调用全局安全工具类进行过滤
String filteredName = SecurityUtils.sanitizeSQL(inputName);
String sql = "SELECT * FROM users WHERE username = '" + filteredName + "'";
return jdbcTemplate.queryForObject(sql, User.class);
}详细审计过程:
- 源头分析:
inputName是从前端传入的用户输入,确实属于不可信来源,CodeQL的判断正确。 - 汇点分析:
jdbcTemplate.queryForObject执行SQL语句,属于危险汇点,CodeQL这里也没毛病。 - 关键链路分析:CodeQL的警报忽略了中间的
SecurityUtils.sanitizeSQL方法。这是我们公司自己开发的一个全局安全工具类,里面的sanitizeSQL方法做了非常彻底的处理——它会把单引号、双引号、分号、注释符等所有SQL注入常用的特殊字符都进行转义(比如把单引号'转成\'),还会过滤掉UNION、DROP、EXEC等危险关键字。也就是说,经过这个方法处理后,filteredName里已经不存在能构成注入的"弹药"了。但CodeQL的默认规则只认识它内置的那些安全函数(比如Apache Commons Lang里的StringEscapeUtils.escapeSql),对于我们这种自定义的过滤方法,它没办法识别其安全性,所以依然会报警报。
结论:假漏洞。这种情况需要我们人工介入,去核查中间的过滤函数到底有没有真的起到"净化"作用。如果这个自定义过滤器确实能有效拦截恶意输入,那这个警报就是误报。
4.3 无效的Sink
CodeQL报告:在 FileProcessor.java 的第112行,检测到用户控制的URL参数可能导致SSRF(服务器端请求伪造)漏洞。
我们来看代码:
public void logFileUrl(@RequestParam String fileUrl) {
// 仅将URL记录到日志,未发起任何网络请求
logger.info("用户请求处理的文件URL:" + fileUrl);
// 其他业务逻辑...
}详细审计过程:
- 源头分析:
fileUrl是用户传入的参数,完全可控,属于SSRF漏洞的典型源头,CodeQL这一步判断正确。 - 汇点分析:这是问题的核心。CodeQL的SSRF检测规则里,可能把所有"接收字符串并输出"的函数都当成了潜在的危险汇点,但实际上,SSRF的危害在于服务器会根据这个URL发起网络请求(比如访问内部系统、敏感端口等)。而这里的
logger.info方法仅仅是把字符串写入日志文件,它既不会解析这个URL,也不会发起任何HTTP/HTTPS请求,更不可能去连接内部服务。这个"汇点"根本没有执行危险操作的能力,是个"伪Sink"。 - 链路分析:数据从
fileUrl直接拼接进日志字符串,中间没有其他处理,但因为最终的"汇点"不具备发起请求的能力,所以整个链路没有安全风险。
结论:假漏洞。这是由于CodeQL对"危险汇点"的定义可能过于宽泛,把一些看似相关但实际无危害的函数也纳入了监测范围。我们在审计时,一定要确认数据最终流向的函数是否真的能执行对应的危险操作(比如SSRF里的 URL.openConnection、HttpClient.execute,XSS里的 response.getWriter().write 等)。
五、总结
好了,我们来总结一下用CodeQL审计Java代码SQL注入(其他漏洞也类似)的心法:
- 工具是辅助:CodeQL发现的目标是疑似漏洞,需要你自己去最终判别。
- 核心是数据流:一定要亲手去跟踪
Source -> ... -> Sink这条线。不要只看头和尾,中间路径的"净化"环节至关重要。 - 理解框架特性:像MyBatis的
#和$,Spring的参数绑定,这些框架知识能帮你快速判断漏洞的真伪。 警惕误报三点:
- 类型安全(如int参数)。
- 自定义过滤(CodeQL不认识你的安全函数)。
- Sink点误判(数据没流到真正危险的地方)。
通过这种"工具扫描 + 人工研判"的模式,我们就能在复杂的Java项目中,高效、精准地挖出真正的安全漏洞。
作者:汤青松
日期:2025年11月17日
微信:songboy8888
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。