CodeQL使用

参考:CodeQL Java/Kotlin 官方文档

1.基本使用

1.1 CodeQL 安装

CodeQL 的常用工具包括 CodeQL CLI、标准查询与库包,以及 Visual Studio Code 扩展。

CLI 用于创建数据库、执行查询和分析结果。GitHub 提供的 CodeQL Bundle 已包含 CLI 以及官方维护的标准查询和库包,通常优先下载 Bundle,不需要再单独克隆查询仓库。

标准查询和库包是开源的,既可以直接使用,也可以作为编写自定义查询的基础。

1.1.1 CLI 安装

首先在系统上选定CodeQL的安装位置。

从 CodeQL CLI Releases 下载适合当前系统的 CodeQL Bundle,解压到 ~/CodeQL 等固定目录。

为方便调用,可将 codeql 可执行文件所在目录加入 PATH:

image.png

1.2 查询与库包

CodeQL Bundle 已包含官方标准查询和库包。只有在需要阅读最新源码、参与开发或使用仓库中特定版本时,才需要额外克隆 github/codeql;旧的 Semmle/ql 地址不再作为首选入口。

1.3.VSCode开发插件安装

CodeQL需要使用Visual Studio Code来开发和调试规则,所以我们需要在VSCode上面安装CodeQL的插件。

我们安装好Visual Studio Code后,在它的扩展里面搜索codeql, 点击安装。

image.png

配置一下上面我们安装的codeql引擎路径,设置好了CodeQL的开发环境。

image.png

2.CodeQL简单使用

用的是这个靶场 —— micro_service_seclab:

生成数据库

CodeQL的核心引擎是不开源的,这个核心引擎的作用之一是帮助我们把micro-service-seclab转换成CodeQL能识别的中间层数据库。

codeql database create E:\Coding\CodeQL\CodeQL-Practice –language=”java” –source-root=E:\Coding\CodeQL\micro_service_seclab –command=”mvn clean package -Dmaven.test.skip=true”

codeql database create /CodeQL/databases/codeql_demo 当然是指我们要创建的database为/CodeQL/databases/codeql_demo(注意:要先创建~/CodeQL/databases/目录)。

--language=”java” 表示当前程序语言为Java。

--command=”mvn clean install –file pom.xml” 编译命令(因为Java是编译语言,所以需要使用–command命令先对项目进行编译,再进行转换,python和php这样的脚本语言不需要此命令)

--source-root=~/CodeQL/micro-service-seclab/ 这个当然指的是项目路径

然后我们需要编写QL查询语句来获取我们想要的数据

image.png

由于CodeQL开源了所有的规则和规则库部分,所以我们能够做的就是编写符合我们业务逻辑的QL规则

类似SQL语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java 	#导入Java的标准 CodeQL 库

from int i #from我理解的选择需要的类型并定义一个变量
where i = 1 #条件语句
select i #输出

关键字as后跟名称。这会为结果列添加“标签”,并允许您在后续的 SELECT 表达式中使用这些标签。
关键字后跟结果列的名称,以及可选的关键字或。这决定了结果的显示顺序。order by y desc

from int x, int y
where x = 3 and y in [0 .. 2]
select x, y, x * y as product, "product: " + product
order by y desc

基本语法包含3个部分。

名称类名 解释 代码中的对应形态
Method
(方法类)
定义/蓝图。它代表了程序员写在类里的那段“方法代码”本身。放在 from
中表示遍历项目中所有声明的方法。
public void getUser(String name) { ... }
(这就是一个 Method)
MethodCall
(方法调用类)
动作/执行。它代表了代码在某个地方“执行”了上述方法的行为。放在 from
中表示遍历项目中所有发生的方法调用动作。
db.getUser("admin");
(这就是一个 MethodCall)
Parameter
(参数类)
变量坑位。它代表了方法定义时,括号里写的那些用于接收数据的变量名(形参)。 public void getUser(String name) 中的 String name

假设我们的目标是:“我想找到所有调用了 **getUser** 方法的地方,并且想知道这个方法定义的第一个参数叫什么名字。”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java

// 1. 声明我们需要用到的三种基本节点
from Method m, MethodCall call, Parameter p
where
// 【Method 的作用】:精确定位我们要找的方法
m.getName() = "getStudent" and

// 【MethodCall 的作用】:把“调用动作”和方法绑定起来。
// 意思是:这个 call 调用的必须是前面那个名为 getStudent 的方法
call.getMethod() = m and

// 【Parameter 的作用】:获取方法的第一个参数(索引为 0)
p = m.getParameter(0)

// 最终输出:调用的位置,以及对应的参数名
select call , "发现了一次调用!这个方法定义的参数名叫: " + p.getName()

image.png

3.CodeQL语法基本规则

3.1.谓词

官方文档:https://codeql.github.com/docs/ql-language-reference/predicates/

本质上就是 Java 里的方法(Method),或者是 SQL 里的过滤条件(WHERE)。

三种形态:

形态 像 Java 里的 例子
独立谓词(无归属) 静态方法 predicate isAdmin(string u) { u = "admin" }
成员谓词(挂在类上) 成员方法 m.getName()、c.getArgument(0)
特征谓词(类的构造函数) 构造函数 MyClass() { this.isPublic() }

​

独立谓词

和SQL一样,where部分的查询条件如果过长,会显得很乱。CodeQL提供一种机制可以让你把很长的查询语句封装成函数。

分两种,有返回值和没有返回值

最基础的:**predicate**开头,不返回东西(只返回 true/false)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java
import semmle.code.java.dataflow.DataFlow

predicate test(int a) {
a=1
}

from int c
where test(c)
select c

from int c:CodeQL 会枚举所有可能的 int值(从 -2147483648到 2147483647)。
where test(c):对于每一个 c,检查 test(c)是否成立。test(c)的定义是 c = 1。
select c:只保留那些让 test(c)成立的 c,也就是 c = 1。

image.png

带 result 的:把 **predicate**换成返回类型

想找所有“返回类型是 String且名字以 get开头的方法”:

1
2
3
4
5
6
7
8
9
10
import java

string test(Method m) {
m.getName().matches("get%") and
result = m.getReturnType().toString()
}

from Method m
where exists(test(m))
select m, test(m)

image.png

成员谓词

常用的

谓词 用在什么类型上 作用 例子
.getName() Method、Class、Field、Parameter等 拿名字 m.getName() = "executeQuery"
.getDeclaringType() Method、Constructor、Field 拿到声明这个东西的类 c.getDeclaringType().hasQualifiedName("java.io", "FileReader")
.getMethod() MethodCall 拿到这个调用点调的是哪个方法 call.getMethod().getName() = "query"
.getArgument(i) Call 拿到第 i 个实参(从0开始) call.getArgument(0)
.getAParameter() Method、Constructor 拿到其中一个形参(可能多个) `exists(Parameter p \
.getAnAnnotation() Annotatable(方法、类、字段等) 拿到其中一个注解 m.getAnAnnotation().getType().hasName("Override")
.getType() 各种表达式、变量 拿到类型 p.getType().hasName("String")
.hasName(s) Method、Class、Type等 名字是否等于 s type.hasName("String")
.hasQualifiedName(pkg, cls) RefType 全限定名是否匹配 type.hasQualifiedName("java.io", "File")

处理集合和条件时常用

谓词/关键字 作用 例子
exists(...) “有没有至少一个?” `exists(Parameter p \
forall(...) “是不是全都满足?” `forall(Parameter p \
none(...) “是不是一个都没有?” `none(Parameter p \
instanceof 检查类型 src instanceof StringLiteral
matches(pattern) 字符串模糊匹配(% 是通配符) s.matches("%admin%")
regexpMatch(pattern) 正则匹配 s.regexpMatch(".*\\$\\{.*")

数据流分析专用

谓词 作用 用在什么地方
DataFlow::exprNode(e) 把表达式转成数据流节点 把 Expr塞进 localFlow
DataFlow::parameterNode(p) 把参数转成数据流节点 同上
DataFlow::localFlow(src, sink) 同一方法内,值能从 src 流到 sink 吗 局部数据流分析
isSource(node) 定义“污点从哪里来” 写在 TaintTracking::Configuration里
isSink(node) 定义“污点流到哪里算危险” 同上
hasFlow(src, sink) 全局范围内,有没有一条污点路径 执行全局污点追踪
hasFlowPath(src, sink) 同上,但能输出路径(画线) 路径查询

3.2.类

这里的 Method就是一个类,它代表数据库中所有方法定义的集合。m是集合里的一个具体元素(一个方法)。

当自己定义类时,通常是为了:

  1. 给某个类型加一个更精细的筛选条件。
  2. 把多个相关类型统一成一个抽象类(比如 SqlExpr)。
  3. 给已有的类添加新的成员谓词(通过继承)。

**Method**、**IfStmt**、**Parameter**这些都是类。比如:

1
2
from Method m
select m

这里的 Method就是一个类,它代表数据库中所有方法定义的集合。m是集合里的一个具体元素(一个方法)。

官方文档说:“类是一组值的集合。” 对,Method就是所有方法值的集合。

image.png

类是怎么定义的

1
2
3
4
5
6
7
8
9
10
11
12
class OneTwoThree extends int {
OneTwoThree() {
this = 1 or this = 2 or this = 3
}
}

class OneTwoThree extends int:我定义一个新类叫 OneTwoThree,它是 int的一个子集(所有 OneTwoThree的值也都是整数)。
OneTwoThree() { ... }:这叫特征谓词,相当于“这个类的入场券”。只有满足花括号里条件的整数,才属于这个类。
this = 1 or this = 2 or this = 3:条件就是“这个值等于 1、2 或 3”。

所以,OneTwoThree这个类里只有三个值:1、2、3。其他整数(比如 4、5)不属于它。

类的成员谓词

类里面可以定义成员谓词,就像 Java 类里的方法。官方例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class OneTwoThree extends int {
OneTwoThree() {
this = 1 or this = 2 or this = 3
}

string getAString() {
result = "One, two or three: " + this.toString()
}
}

getAString()是一个成员谓词,它返回一个字符串。
只有属于 OneTwoThree的值才能调用它。
调用方式:(OneTwoThree).getAString()或 1.(OneTwoThree).getAString()(先把 1 转型为 OneTwoThree)。
成员谓词:m.getName()、c.getArgument(0)、p.getCallable()等这些全是成员谓词。

抽象类

官方例子:你想描述“所有可能被解释为 SQL 查询的表达式”。不同数据库(PostgreSQL、MySQL)的 SQL 表达式不同,但你可以定义一个抽象类 SqlExpr:

1
2
3
abstract class SqlExpr extends Expr {
// 不写特征谓词,因为它是抽象的
}

然后为每种数据库定义子类:

1
2
class PostgresSqlExpr extends SqlExpr { ... }
class MySqlExpr extends SqlExpr { ... }

抽象类的好处:你可以在 SqlExpr上定义通用的成员谓词,然后所有子类都能用。如果需要支持新数据库,只需加一个子类,不用改已有查询。

类比:

“动物”是一个抽象类,它包含“狗”、“猫”、“鸟”等子类。你可以定义“动物都会叫”,然后狗叫汪汪,猫叫喵喵。

继承和覆盖

子类可以覆盖(override)父类的成员谓词。官方例子:

1
2
3
4
5
6
7
8
9
class OneTwo extends OneTwoThree {
OneTwo() {
this = 1 or this = 2
}

override string getAString() {
result = "One or two: " + this.toString()
}
}
  • OneTwo继承了 OneTwoThree,但覆盖了 getAString()。
  • 对于值 1 和 2,调用 getAString()时会使用 OneTwo的版本(“One or two: …”),而不是 OneTwoThree的版本。
  • 对于值 3,它不属于 OneTwo,所以仍用 OneTwoThree的版本。

关键点:CodeQL 会根据值所属的最具体类来决定调用哪个版本的谓词。这叫“多态”。

类和数据库类型的关系

用的 Method、IfStmt这些类,实际上是 CodeQL 库预先定义好的,它们对应数据库里的表。比如 Method对应数据库里的 @method表。你不需要自己定义它们,直接拿来用就行。

当你自己定义类时,通常是为了:

  1. 给某个类型加一个更精细的筛选条件(比如 OneTwoThree只包含 1、2、3)。
  2. 把多个相关类型统一成一个抽象类(比如 SqlExpr)。
  3. 给已有的类添加新的成员谓词(通过继承)。

和Java中的类来举例子

区别一、Java 类是用来“创建对象”的,CodeQL 类是用来“筛选值”的

1
2
3
4
5
6
7
8
9
class PublicMethod extends Method {
PublicMethod() {
this.isPublic() // 特征谓词:筛选出所有 public 的方法
}
}

// 使用:不是 new,而是 from 枚举
from PublicMethod pm
select pm

image.png

关键区别:

  • Java:new xxxx(...)创建了一个新的对象。
  • CodeQL:PublicMethod类不是创建新方法,而是从已有的所有方法中挑出那些 public 的。

​

区别二、特征谓词是 CodeQL 独有

CodeQL 的特征谓词是定义准入条件的:

1
2
3
PublicMethod() {
this.isPublic() // 只有 public 的方法才能进入这个类
}

特征谓词不是“创建”,而是“判断”。它不产生新东西,只是决定哪些已有的值能进这个类。Java是直接new一个对象出来。

区别三:CodeQL 的类不封装数据,只封装查询逻辑

CodeQL 的类不能存数据,只能定义查询逻辑(成员谓词):

1
2
3
4
5
6
7
class PublicMethod extends Method {
PublicMethod() { this.isPublic() }

string getDescription() {
result = "Public method: " + this.getName()
}
}

getDescription()不是从字段里读数据,而是从已有的 Method 节点上计算出来的。

image.png

3.3.量词

在查询里临时引入一些变量,用来检查“有没有”或“是不是全都”满足某个条件。

exists(变量声明 | 条件)

forall(变量声明 | 范围条件 | 检查条件)

forex(变量声明 | 范围条件 | 检查条件)

  • 变量声明:临时变量,比如 int i、Parameter p。
  • 范围条件(仅 forall/forex):限定变量的取值范围,比如 p = m.getAParameter()。
  • 检查条件:对范围内的每个变量进行检查,比如 p.getType().hasName("String")。

1.exists

exists(变量声明 | 条件)

最常用,用来检查是否存在至少一个满足条件的值。

这个方法有没有一个形参是 String 类型

1
exists(Parameter p | p = m.getAParameter() and p.getType().hasName("String"))

2.forall

forall(变量声明 | 范围条件 | 检查条件)

用来检查是否存在全部满足条件的值。

这个方法的所有形参,是不是全都是 String 类型

注意:如果这个方法没有形参,forall会认为“全都满足” → true。这叫做“空真”。

1
forall(Parameter p | p = m.getAParameter() | p.getType().hasName("String"))

3.forex

forex(变量声明 | 范围条件 | 检查条件)

检查“至少有一个,且全都”

forex就是 forall+ exists的组合,避免了空真问题。

这个方法至少有一个形参,并且所有形参都是 String 类型。

1
forex(Parameter p | p = m.getAParameter() | p.getType().hasName("String"))

3.4.AST 与 注解

Java AST语法树

官方的说法:AST 代表程序的语法结构,节点代表语句和表达式。

AST 节点 = 代码零件的名字,写查询就是按名字找零件

语句(Stmt) 是动作(if、while、return),表达式(Expr) 是值(加法、变量、调用)。

遇到不认识的节点,就打开 AST 视图看一眼,比看文档快十倍

自己查看AST语法树操作步骤:

  1. 打开一个 Java 文件。
  2. 右键 → CodeQL: View AST。
  3. 在 AST 树里,随便点一个节点,看它的类名。
  4. 对照上面的表格,你就知道这个节点叫什么了。

image.png

​

语句(Stmt)和表达式(Expr)

阵营 代表什么 例子 特点
Stmt(语句) 一句完整的“命令”,以分号或花括号结尾 if、while、return、{ } 不产生值,只执行动作
Expr(表达式) 一个“值”,可以计算、赋值、传参 1 + 2、"hello"、obj.foo() 产生一个值,可以嵌套在语句里

语句类(Stmt)

官方表格里列了一大堆,但你只需要记住最常用的 8 个(其他的遇到再查):

你写的 Java 代码 CodeQL 类名 你什么时候用
if (cond) ... IfStmt 找所有 if
while (cond) ... WhileStmt 找所有 while
for (...;...;...) ... ForStmt 找普通 for
for (Type x : list) ... EnhancedForStmt 找 foreach
return expr; ReturnStmt 找 return
throw expr; ThrowStmt 找 throw
try { ... } catch ... TryStmt 找 try
{ ... } BlockStmt 找代码块

表达式类(Expr)

字面量(Literal)—— 直接写死的值

代码 CodeQL 类 例子
true/ false BooleanLiteral from BooleanLiteral b
23 IntegerLiteral 整数常量
"hello" StringLiteral 字符串常量
null NullLiteral null

一元运算(UnaryExpr)—— 单个操作数

代码 CodeQL 类 例子
x++ PostIncExpr 后置自增
++x PreIncExpr 前置自增
!flag LogNotExpr 逻辑非
-num MinusExpr 负号

二元运算(BinaryExpr)—— 两个操作数

代码 CodeQL 类 例子
a + b AddExpr 加法
a - b SubExpr 减法
a == b EQExpr 相等判断
a && b AndLogicalExpr 逻辑与

赋值运算(AssignExpr 等)

代码 CodeQL 类
x = 1 AssignExpr
x += 1 AssignAddExpr
x *= 2 AssignMulExpr

访问和调用(Access)

代码 CodeQL 类 说明
this ThisAccess 当前对象
x VarAccess 变量访问
obj.field FieldAccess 字段访问
arr[i] ArrayAccess 数组下标
foo() MethodCall 方法调用
new Foo() ClassInstanceExpr 创建对象

其他常用

代码 CodeQL 类 说明
(int)x CastExpr 类型转换
x instanceof String InstanceOfExpr 类型检查
cond ? a : b ConditionalExpr 三目运算符
String.class TypeLiteral 类字面量
@Override Annotation 注解

注解

注解在 CodeQL 里的四个零件

零件 官方类名 解释 例子
能被贴标签的东西 Annotatable 任何可以写 @XXX的地方:类、方法、字段、参数、局部变量、包 class Foo、void bar()、String name
标签的种类 AnnotationType 注解的“定义”,比如 @Override这个标签本身是什么 java.lang.Override、java.lang.Deprecated
标签里的填空项 AnnotationElement 注解里的小括号参数,比如 @SuppressWarnings("rawtypes")里的 "rawtypes" value、name
贴上去的那张纸 Annotation 代码里实际写出来的 @XXX(...)这个具体实例 @Override、@SuppressWarnings("deprecation")

例子:找 **@SuppressWarnings**

1
2
3
4
5
6
7
8
9
10
11
12
13
import java

#找“构造器”和“注解”这两个东西。
from Annotation ann,Constructor c
where
ann=c.getAnAnnotation() and #这个注解 ann是贴在这个构造器 c身上的。
ann.getType().hasQualifiedName("java.lang", "SuppressWarnings") @这个注解的种类是 java.lang.SuppressWarnings。

select c,ann,ann.getValue("value") #输出构造器、注解本身、以及注解里 value这个填空项的值(比如 "rawtypes")

getAnAnnotation()是 Annotatable的成员谓词,用来拿贴在身上的所有注解。
getType()拿到注解的类型(AnnotationType)。
getValue("value")拿到注解里某个填空项的值。

image.png

第二个例子:找“应该写 **@Override**却没写”的方法

第一步:定义“什么是 **@Override**注解”

1
2
3
4
5
class OverrideAnnotation extends Annotation {
OverrideAnnotation() {
this.getType().hasQualifiedName("java.lang", "Override")
}
}

造了一个新类叫 OverrideAnnotation,它只包含那些种类是 java.lang.Override的注解。

第二步:写查询

1
2
3
4
5
from Method overriding, Method base
where
overriding.overrides(base) and
not exists(OverrideAnnotation a | a = overriding.getAnAnnotation())
select overriding, "Should have @Override annotation"
  1. overriding.overrides(base):方法 overriding重写了方法 base。
  2. not exists(...):不存在一个 OverrideAnnotation类型的注解贴在 overriding身上。
  3. 结果:所有重写了父类方法、但自己没写 @Override的方法。

​

第三个例子:找“调用废弃方法”的地方(并排除合理情况)

  1. 定义“废弃方法”:贴了 @Deprecated注解的方法。
  2. 找所有调用这些废弃方法的地方。
  3. 排除那些调用方自己也是废弃的。
  4. 再排除那些调用方写了 **@SuppressWarnings("deprecation")** 的。
1
2
3
4
5
6
7
8
9
10
11
class DeprecatedAnnotation extends Annotation {
DeprecatedAnnotation() {
this.getType().hasQualifiedName("java.lang", "Deprecated")
}
}

class DeprecatedMethod extends Method {
DeprecatedMethod() {
exists(DeprecatedAnnotation a | a = this.getAnAnnotation())
}
}

然后找调用:instanceof

1
2
3
4
from MethodCall call, DeprecatedMethod dm
where call.getMethod() = dm
and not call.getEnclosingCallable() instanceof DeprecatedMethod
select call, "Call to deprecated method"
  • call.getMethod() = dm:这个调用调的是废弃方法。
  • call.getEnclosingCallable():调用所在的上级方法(谁调了它)。
  • not ... instanceof DeprecatedMethod:这个上级方法本身不是废弃的。

再升级:排除 @SuppressWarnings("deprecation")

1
2
3
4
5
6
class SuppressDeprecationWarning extends Annotation {
SuppressDeprecationWarning() {
this.getType().hasQualifiedName("java.lang", "SuppressWarnings") and
exists(string s | s = this.getAStringArrayValue("value") and s.matches("deprecation"))
}
}

然后在查询里加:

1
and not exists(SuppressDeprecationWarning sw | sw = call.getEnclosingCallable().(Annotatable).getAnAnnotation())

3.4.Java 数据流与污点追踪

就是数据流是追踪“值”在代码里怎么走的,污点追踪是追踪“脏东西”怎么传染的。

为什么需要数据流分析?

查询query方法被调用,比如:

1
2
3
4
5
import java

from MethodCall c
where c.getMethod().getName()="query"
select c

这只能找到调用 query 的地方,但看不到参数是从哪来的。比如:

1
2
3
4
5
6
public List<Student> getStudent(String username) {
//String sql = "select * from students where username like '%" + username.get() + "%'";
String sql = "select * from students where username like '%" + username + "%'";
//String sql = "select * from students where username like ?";
return jdbcTemplate.query(sql, ROW_MAPPER);
}

光靠“找形状”抓不到 username是用户输入。数据流分析就是顺着代码的赋值、传参、返回值,追踪一个值从出生到使用的地方。

image.png

​

局部数据流:在一个方法里追

核心概念:Node(节点)

数据流分析把代码里的每个“值”出现的地方看作一个节点(Node)。节点有两种:

  • 表达式节点(ExprNode):比如 userId、"hello"、a + b
  • 参数节点(ParameterNode):比如方法参数 String userId

你可以用 DataFlow::exprNode(expr)把一个表达式转成节点,用 DataFlow::parameterNode(param)把参数转成节点。

localFlow:同方法内追

DataFlow::localFlow(src, sink)问:在同一个方法里,值能从 **src**流到 **sink****吗?**​

1
2
3
4
void foo(String userInput) {
String sql = "SELECT * FROM users WHERE name = '" + userInput + "'";
stmt.executeQuery(sql);
}
  • src= userInput(参数节点)
  • sink= sql(表达式节点,即 executeQuery的第一个实参)
  • localFlow会告诉你:能,因为 userInput被拼接到 sql里,然后传给 executeQuery。

一个简单例子:找 **JSON.parseObject**的参数来源

1
2
3
4
5
6
7
8
9
import java
import semmle.code.java.dataflow.DataFlow

from Call call, Expr src
where
call.getCallee().(Method).hasQualifiedName("com.alibaba.fastjson", "JSON", "parseObject") and
DataFlow::localFlow(DataFlow::exprNode(src), DataFlow::exprNode(call.getArgument(0)))
select src, call.getArgument(0)

Call是所有调用点的基类,包括方法调用和构造器调用。

call.getCallee()返回被调用的目标(可能是 Method或 Constructor),我们通过 .(Method)将其转型为 Method,然后调用 hasQualifiedName来匹配全限定方法名。

hasQualifiedName(“com.alibaba.fastjson”, “JSON”, “parseObject”)的三个参数分别是:包名、类名、方法名。

其余部分不变:追踪第一个参数的数据流来源。

image.png

局部污点追踪:不仅追值,还追“脏”

数据流 vs 污点追踪

概念 追踪什么 例子
数据流(data flow) 值本身 x = 1; y = x;→ y 的值就是 1
污点追踪(taint tracking) “脏不脏” x = userInput; y = "prefix" + x;→ y 的值不是 userInput,但它“沾了脏”

关键区别:数据流只认“值相等”,污点追踪认为“只要经过拼接、替换、子串等操作,脏属性会传播”。

localTaint:同方法内追脏

DataFlow::localTaint(src, sink)问:在同一个方法里,脏东西能从 **src**传播到 **sink**吗?

比如:

1
2
3
String x = userInput;          // x 脏
String y = x.trim(); // y 也脏(trim 不改变脏属性)
String z = y + "safe"; // z 也脏(拼接后仍脏)

localTaint会认为 x → y → z都是脏的,即使值本身变了。

好的,我来把官方这篇关于数据流和污点追踪的文档,用你能听懂的大白话重新讲一遍。核心就一句话:数据流是追踪“值”在代码里怎么走的,污点追踪是追踪“脏东西”怎么传染的。

全局数据流:跨方法追

局部追踪只能在一个方法里玩,但在真实的 Spring Boot 项目中,漏洞往往是跨方法的:

1
2
3
4
5
6
7
8
9
10
// Controller
public void handle(String input) {
service.process(input); // 跨方法调用
}

// Service
public void process(String data) {
String sql = "SELECT * FROM users WHERE name = '" + data + "'"; // 拼接
db.execute(sql);
}

这里 input 从 Controller 一路跑到 Service,还经历了字符串拼接。局部流追不到,因为跨越了方法边界。我们要用 **TaintTracking::Global**(全局污点追踪)来搞定它。

全局数据流配置

你需要定义一个“配置”,告诉引擎:

  • source(源头):数据从哪来(比如方法参数、用户输入)
  • sink(终点):数据流到哪算危险(比如 executeQuery的参数

一句话核心总结:

在 CodeQL 里,数据流(DataFlow)是追踪“一模一样的值”在代码里怎么跑的;污点追踪(TaintTracking)是追踪“脏东西(被污染的属性)”是怎么传染的。

全局污点追踪写法(模块化 API):

你需要定义一个“配置模板”,告诉引擎“源头在哪”和“终点在哪”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java
import semmle.code.java.dataflow.TaintTracking // 必须引入 TaintTracking

// 1. 定义配置模板(注意:新版 API 绝对不要写 override!)
module MyConfig implements DataFlow::ConfigSig {

// 定义源头 (Source)
predicate isSource(DataFlow::Node node) {
// 比如:把名字叫 handle 的方法的参数当源头
exists(Method m |
m.getName() = "handle" and
node.asParameter() = m.getAParameter()
)
}

// 定义终点 (Sink)
predicate isSink(DataFlow::Node node) {
// 比如:把执行数据库的 execute 方法的参数当终点
exists(MethodCall ma |
ma.getMethod().getName() = "execute" and
node.asExpr() = ma.getArgument(0)
)
}
}

// 2. 将配置交给全局污点追踪引擎
module Flow = TaintTracking::Global<MyConfig>;

// 3. 执行查询
from DataFlow::Node src, DataFlow::Node sink
where Flow::flow(src, sink) // 让引擎帮你算出跨方法的全局路径
select src, sink, "警告:发现跨方法的污点传播漏洞!"

大白话总结: 挖真实漏洞时,99% 的情况下你都会用 **TaintTracking**(污点追踪)而不是普通的 **DataFlow**。你只需要套用上面的模板,修改里面的 isSource 和 isSink,引擎就会自动帮你穿透一层层的方法调用,把隐藏极深的漏洞揪出来!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java
import semmle.code.java.dataflow.TaintTracking

module MyConfig implements DataFlow::ConfigSig {

// 源头 (Source):假设数据依然是从 Spring Boot 的 Controller 接口进来的
predicate isSource(DataFlow::Node node) {
exists(Method m |
// 抓取类似 @PostMapping, @GetMapping 等常用入口
m.getAnAnnotation().getType().hasName("PostMapping") and
node.asParameter() = m.getAParameter()
)
}

// 终点 (Sink):核心高能区域!专门抓 JdbcTemplate 的 SQL 注入
predicate isSink(DataFlow::Node node) {
exists(MethodCall ma, Method m |
ma.getMethod() = m and

// 1. 精准定位类:必须是 Spring 的 JdbcTemplate 类
m.getDeclaringType().hasQualifiedName("org.springframework.jdbc.core", "JdbcTemplate") and

// 2. 定位方法:匹配以 query、update 或 execute 开头的方法
// 使用 matches 可以把 queryForList, queryForObject 等一网打尽
m.getName().matches("query%") and

// 3. 危险参数:JdbcTemplate 的 SQL 语句永远是第一个参数(索引为 0)
node.asExpr() = ma.getArgument(0)
)
}
}

// 交给污点追踪引擎
module Flow = TaintTracking::Global<MyConfig>;

from DataFlow::Node src, DataFlow::Node sink
where Flow::flow(src, sink)
select sink, src, "发现了一条直达 JdbcTemplate 的高危 SQL 注入路径!"

image.png

3.5.Java 安全审计实战规则库总目录

GitHub 官方维护的 Java 安全审计规则库——里面全是 .ql查询文件,按 CWE 编号分类,每个文件就是一条“检测某种漏洞模式”的现成规则。

https://github.com/github/codeql/tree/main/java/ql/src/Security

目录结构拆解

codeql/java/ql/src/Security/

├── CWE/ ← 核心,按 CWE 编号分目录

│ ├── CWE-020/ ← 外部输入不当(Log4j 那种算这)

│ ├── CWE-022/ ← 路径穿越(../ 那种)

│ ├── CWE-078/ ← OS 命令注入(Runtime.exec)

│ ├── CWE-079/ ← XSS

│ ├── CWE-089/ ← SQL 注入(JPA/MyBatis/Hibernate)

│ ├── CWE-117/ ← 日志注入(Log4j 也沾边)

│ ├── CWE-209/ ← 错误信息泄露

│ ├── CWE-276/ ← 文件权限不当

│ ├── CWE-295/ │ 证书校验

│ ├── CWE-319/ │ 明文传输

│ ├── CWE-328/ │ 弱加密

│ ├── CWE-330/ │ 弱随机数

│ ├── CWE-338/ │ SecureRandom

│ ├── CWE-477/ │ 使用过时函数

│ ├── CWE-502/ ← ★ 反序列化(CC/Commons-Beanutils/Fastjson 都在这)

│ ├── CWE-611/ ← XXE

│ ├── CWE-614/ │ 不安全的 Cookie

│ ├── CWE-643/ ← XPath 注入

│ ├── CWE-798/ │ 硬编码凭证

│ ├── CWE-829/ │ 组件依赖漏洞(SCA 向)

│ └── …更多

├── qlpack.yml ← 这个包的元数据(依赖、名称)

├── definitions.ql ← 一些共享定义

└── codeql-suites/ ← 可以一次性跑的 suite 文件

使用规则库扫描

1
2
3
codeql database analyze <你的数据库路径> \
codeql/java/ql/src/Security/ \
--format=csv --output=results.csv

跑完 results.csv里就是所有命中的漏洞点位(文件、行号、CWE 编号、置信度)

如果想跑部分的

image.png

​

使用部分规则库扫描

1
2
3
4
5
codeql database analyze <数据库路径> \
codeql/java/ql/src/Security/CWE/CWE-502/UnsafeDeserialization.ql \
codeql/java/ql/src/Security/CWE/CWE-078/CommandInjection.ql \
codeql/java/ql/src/Security/CWE/CWE-089/SqlInjection.ql \
--format=sarif-latest --output=results.sarif.json

VS Code 里更简单:在 CWE-502目录里随便点一个 .ql右键 → “CodeQL: Run Query”,只跑那一条。

自己写个 mini suite(推荐,最实用)

suite 文件就是个 YAML,用来打包你想跑的查询。在随便一个目录(比如你项目根)新建 my-audit.qls:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# ============================================================
# 文件名: fast-web-security.qls
# 用途: 只扫描 Web 安全相关的高精度漏洞规则(适合 Java 项目)
# 使用方法:
# codeql database analyze <数据库路径> fast-web-security.qls --format=csv --output=web_results.csv
# ============================================================

# Suite 文件的名称(给人看的,随便写)
- description: "Fast Web Security (High Precision Only)"

# ---------- 1. 指定要扫描的 CWE 目录 ----------
# 每个 - queries: 指向一个 CWE 文件夹,CodeQL 会扫描该文件夹下所有 .ql 文件
# 注意:路径是相对于 codeql/java/ql/src/ 的,如果你的 codeql 包安装位置不同,可能需要写绝对路径
# 推荐写法:直接写 codeql/java/ql/src/Security/CWE/CWE-XXX (如果你的 codeql 包在标准位置)
# 如果不行,可以写相对路径(相对于 qlpack.yml 所在目录),但容易出错,不建议

- queries: codeql/java/ql/src/Security/CWE/CWE-022 # 路径穿越(../)
- queries: codeql/java/ql/src/Security/CWE/CWE-023 # 相对路径穿越
- queries: codeql/java/ql/src/Security/CWE/CWE-074 # 路径注入
- queries: codeql/java/ql/src/Security/CWE/CWE-079 # XSS(跨站脚本)
- queries: codeql/java/ql/src/Security/CWE/CWE-089 # SQL 注入(MyBatis/JPA 等)
- queries: codeql/java/ql/src/Security/CWE/CWE-094 # 代码注入(如 SpEL、Groovy)
- queries: codeql/java/ql/src/Security/CWE/CWE-113 # HTTP 响应拆分
- queries: codeql/java/ql/src/Security/CWE/CWE-134 # 格式化字符串注入
- queries: codeql/java/ql/src/Security/CWE/CWE-200 # 信息泄露(敏感信息暴露)
- queries: codeql/java/ql/src/Security/CWE/CWE-209 # 错误信息泄露(堆栈信息)
- queries: codeql/java/ql/src/Security/CWE/CWE-352 # CSRF(跨站请求伪造)
- queries: codeql/java/ql/src/Security/CWE/CWE-601 # 开放重定向
- queries: codeql/java/ql/src/Security/CWE/CWE-611 # XXE(XML 外部实体注入)
- queries: codeql/java/ql/src/Security/CWE/CWE-918 # SSRF(服务端请求伪造)

# ---------- 2. 二次过滤:只保留高精度规则 ----------
# include 是对上面所有 queries 的进一步筛选
# 只有同时满足以下条件的 .ql 文件才会被实际运行:
# - 标签中包含 "security"(官方 Security 目录下的文件基本都有)
# - precision(精度)为 high 或 very-high(误报率低的)
# 这样能大幅减少噪音,让你优先关注真正需要人工验证的点

- include:
tags contain: security
precision:
- high
- very-high

然后跑:

1
2
3
codeql database analyze <数据库路径> \
my-audit.qls \
--format=sarif-latest --output=results.sarif.json