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值(从 -21474836482147483647)。
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() MethodClassFieldParameter 拿名字 m.getName() = "executeQuery"
.getDeclaringType() MethodConstructorField 拿到声明这个东西的类 c.getDeclaringType().hasQualifiedName("java.io", "FileReader")
.getMethod() MethodCall 拿到这个调用点调的是哪个方法 call.getMethod().getName() = "query"
.getArgument(i) Call 拿到第 i 个实参(从0开始) call.getArgument(0)
.getAParameter() MethodConstructor 拿到其中一个形参(可能多个) `exists(Parameter p \
.getAnAnnotation() Annotatable(方法、类、字段等) 拿到其中一个注解 m.getAnAnnotation().getType().hasName("Override")
.getType() 各种表达式、变量 拿到类型 p.getType().hasName("String")
.hasName(s) MethodClassType 名字是否等于 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:条件就是“这个值等于 123”。

所以,OneTwoThree这个类里只有三个值:123。其他整数(比如 45)不属于它。

类的成员谓词

类里面可以定义成员谓词,就像 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 会根据值所属的最具体类来决定调用哪个版本的谓词。这叫“多态”。

类和数据库类型的关系

用的 MethodIfStmt这些类,实际上是 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 iParameter 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(语句) 一句完整的“命令”,以分号或花括号结尾 ifwhilereturn{ } 不产生值,只执行动作
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 Foovoid bar()String name
标签的种类 AnnotationType 注解的“定义”,比如 @Override这个标签本身是什么 java.lang.Overridejava.lang.Deprecated
标签里的填空项 AnnotationElement 注解里的小括号参数,比如 @SuppressWarnings("rawtypes")里的 "rawtypes" valuename
贴上去的那张纸 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**。你只需要套用上面的模板,修改里面的 isSourceisSink,引擎就会自动帮你穿透一层层的方法调用,把隐藏极深的漏洞揪出来!

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