2

一、背景

今年在做个AI代码安全审计的项目,代码仓库里十有八九都是Java项目,所以开始研究怎么给Java做代码审计。传统的人工审计,效率低,还容易看花眼。这时候想到了CodeQL。把你代码转换成可查询的数据库,然后用像SQL一样的语法去挖漏洞。

整体思路很简单,就两步:

  1. 用CodeQL扫描,生成一堆漏洞的报告(JSON格式)。
  2. 拆解和分析这些报告,判断这个漏洞到底是"确诊"还是"误诊"。

二、操作步骤

2.1 CodeQL 扫描

简单来说,分三步走:

  1. 创建数据库:这就好比把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会通过编译过程来理解整个代码结构。
  2. 运行查询:拿着写好的"问题清单"(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。
  3. 查看结果:生成的results.sarif文件里,就藏着所有疑似漏洞的线索。

2.2 扫描结果解析

这个SARIF/JSON文件,大家刚开始看可能会觉得眼花缭乱。不过我们实际上只要看核心信息:

  • 漏洞位置:location 字段会精确告诉你,有问题的代码在哪个文件的第几行第几列。
  • 漏洞类型:ruleId 会告诉你它怀疑是啥漏洞,比如 java/sql-injection
  • 数据流路径:这是最核心的部分!codeFlows 里会展示数据是怎么从"源头"(Source,比如用户输入)流到"汇点"(Sink,比如执行SQL语句的方法)的。这是我们接下来审计的重点。

三、判断漏洞真假

CodeQL报出来的不一定是真漏洞,它只是个"高度可疑"的警报。我们需要去验证它。

核心思路就四个字:跟踪数据流。具体来说,要检查三个地方:

  1. 注入点(Source)有效吗? 这个数据真的是来自不可信的用户吗?
  2. 执行点(Sink)有效吗? 这个函数真的能执行危险操作吗?
  3. 中间链路干净吗? 数据从源头到执行点的路上,有没有被"洗干净"(过滤、编码)?

我举几个例子来具体讲。

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查询
}

审计过程:

  1. 源头:name参数来自用户HTTP请求,完全可控。有效!
  2. 汇点:jdbcTemplate.queryForObject 确实会执行SQL语句。有效!
  3. 链路:数据从 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>

审计过程:

  1. 源头:name参数从Java层传入Mapper。
  2. 汇点:MyBatis框架解析 ${name} 时,会直接进行字符串替换。
  3. 链路:直接拼接。

所以只要 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 "密码已重置";
}

详细审计过程:

  1. 源头分析:userId 确实是用户通过HTTP请求传入的参数,从来源上看属于"不可信输入",这一点CodeQL判断是对的。
  2. 汇点分析:jdbcTemplate.update(sql) 方法确实会执行传入的SQL字符串,属于SQL注入的典型危险汇点,CodeQL这里的判断也没问题。
  3. 关键链路分析:问题出在 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);
}

详细审计过程:

  1. 源头分析:inputName 是从前端传入的用户输入,确实属于不可信来源,CodeQL的判断正确。
  2. 汇点分析:jdbcTemplate.queryForObject 执行SQL语句,属于危险汇点,CodeQL这里也没毛病。
  3. 关键链路分析:CodeQL的警报忽略了中间的 SecurityUtils.sanitizeSQL 方法。这是我们公司自己开发的一个全局安全工具类,里面的 sanitizeSQL 方法做了非常彻底的处理——它会把单引号、双引号、分号、注释符等所有SQL注入常用的特殊字符都进行转义(比如把单引号 ' 转成 \'),还会过滤掉 UNIONDROPEXEC 等危险关键字。也就是说,经过这个方法处理后,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);
 // 其他业务逻辑...
}

详细审计过程:

  1. 源头分析:fileUrl 是用户传入的参数,完全可控,属于SSRF漏洞的典型源头,CodeQL这一步判断正确。
  2. 汇点分析:这是问题的核心。CodeQL的SSRF检测规则里,可能把所有"接收字符串并输出"的函数都当成了潜在的危险汇点,但实际上,SSRF的危害在于服务器会根据这个URL发起网络请求(比如访问内部系统、敏感端口等)。而这里的 logger.info 方法仅仅是把字符串写入日志文件,它既不会解析这个URL,也不会发起任何HTTP/HTTPS请求,更不可能去连接内部服务。这个"汇点"根本没有执行危险操作的能力,是个"伪Sink"。
  3. 链路分析:数据从 fileUrl 直接拼接进日志字符串,中间没有其他处理,但因为最终的"汇点"不具备发起请求的能力,所以整个链路没有安全风险。

结论:假漏洞。这是由于CodeQL对"危险汇点"的定义可能过于宽泛,把一些看似相关但实际无危害的函数也纳入了监测范围。我们在审计时,一定要确认数据最终流向的函数是否真的能执行对应的危险操作(比如SSRF里的 URL.openConnectionHttpClient.execute,XSS里的 response.getWriter().write 等)。


五、总结

好了,我们来总结一下用CodeQL审计Java代码SQL注入(其他漏洞也类似)的心法:

  1. 工具是辅助:CodeQL发现的目标是疑似漏洞,需要你自己去最终判别。
  2. 核心是数据流:一定要亲手去跟踪 Source -> ... -> Sink 这条线。不要只看头和尾,中间路径的"净化"环节至关重要。
  3. 理解框架特性:像MyBatis的 #$,Spring的参数绑定,这些框架知识能帮你快速判断漏洞的真伪。
  4. 警惕误报三点:

    • 类型安全(如int参数)。
    • 自定义过滤(CodeQL不认识你的安全函数)。
    • Sink点误判(数据没流到真正危险的地方)。

通过这种"工具扫描 + 人工研判"的模式,我们就能在复杂的Java项目中,高效、精准地挖出真正的安全漏洞。


作者:汤青松
日期:2025年11月17日
微信:songboy8888


汤青松
5.2k 声望8.3k 粉丝

《PHP Web安全开发实战》 作者


引用和评论

0 条评论
评论支持部分 Markdown 语法:**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用 @ 来通知其他用户。

AltStyle によって変換されたページ (->オリジナル) /