CodeQL学习笔记

CodeQL学习笔记,大部分翻译自官方文档

安装

下载CodeQL CLI

https://github.com/github/codeql-cli-binaries/releases

解压至/xxx/CodeQL目录并添加环境变量export PATH=/xxx/CodeQL/codeql:$PATH

下载包含标准库的工作空间

1
2
3
4
5
cd /xxx/CodeQL/
git clone https://github.com/github/vscode-codeql-starter.git
cd vscode-codeql-starter
git submodule update --init --remote
git submodule update --remote #定期执行以更新子模块

安装VSCode CodeQL扩展

VSCode商店搜索安装CodeQL,并在扩展设置中设置CodeQL引擎路径/xxx/CodeQL/codeql/codeql

测试

在VSCode 中,单击文件 > 打开工作区,选择vscode-codeql-starter目录下的vscode-codeql-starter.code-workspace文件
在VSCode左侧选择CodeQL插件页面,选择一个CodeQL数据库,然后在文件vscode-codeql-starter/codeql-custom-queries-java/example.ql单击右键选择CodeQL:Run Query

基础命令

生成数据库

1
codeql database create /xxx/CodeQL/databases/Test --language="java" --source-root=/xxx/Demo --command="mvn clean package -Dmaven.test.skip=true"

/xxx/CodeQL/databases/Test 指定生成的数据库位置
–source-root 项目源码路径
–command 编译命令,PHP和Python等不需要。对于Maven,Ant等项目也可以省略

如果没有指定--command,CodeQL会根据平台的不同,调用./java/tools/autobuild.cmd./java/tools/autobuild.sh对项目进行分析。如果该项目的编译工具为Gradle、Maven或Ant,且能找到相应的配置文件。程序就会进入相应的流程,调用相关的编译指令对项目进行编译。CodeQL会收集项目编译过程中产生的信息,并以此生成数据库。如果不属于Gradle、Maven、Ant中任意一种,则报错退出。

CodeQL元数据

CodeQL查询的元数据作为 QLDoc 注释的内容包含在每个查询文件的顶部。此元数据告诉 LGTM 和VSCode 的 CodeQL 插件如何处理查询并正确显示其结果。

例:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @name Empty block
* @kind problem
* @problem.severity warning
* @id java/example/empty-block
*/

import java

from BlockStmt b
where b.getNumStmt() = 0
select b, "This is an empty block."
1
2
3
/**
* @kind path-problem
*/

一些示例

Java污点跟踪_GetenvSource-URLSink

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
/**
* @kind path-problem
*/

import java
import semmle.code.java.dataflow.TaintTracking
import DataFlow::PathGraph

class GetenvSource extends DataFlow::ExprNode {
GetenvSource() {
exists(Method m | m = this.asExpr().(MethodAccess).getMethod() |
m.hasName("getenv") and
m.getDeclaringType() instanceof TypeSystem
)
}
}

class URLSink extends DataFlow::ExprNode {
URLSink() {
exists(Call call |
this.asExpr() = call.getArgument(0) and
call.getCallee().(Constructor).getDeclaringType().hasQualifiedName("java.net", "URL")
)
}
}

class GetenvToURLTaintTrackingConfig extends TaintTracking::Configuration {
GetenvToURLTaintTrackingConfig() { this = "GetenvToURLTaintTrackingConfig" }

override predicate isSource(DataFlow::Node source) { source instanceof GetenvSource }

override predicate isSink(DataFlow::Node sink) { sink instanceof URLSink }
}

from GetenvToURLTaintTrackingConfig cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink, "-"

Python污点跟踪_RemoteFlowSource-FileSystemAccessSink

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @kind path-problem
*/

import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.TaintTracking
import semmle.python.dataflow.new.RemoteFlowSources
import semmle.python.Concepts
import DataFlow::PathGraph

class RemoteToFileConfiguration extends TaintTracking::Configuration {
RemoteToFileConfiguration() { this = "RemoteToFileConfiguration" }

override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }

override predicate isSink(DataFlow::Node sink) {
sink = any(FileSystemAccess fa).getAPathArgument()
}
}

from RemoteToFileConfiguration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink, "-"

CodeQL for Java

Java代码的基础查询

以下查询为查找多余的if语句,即then分支是空的,如if (...) { }

1
2
3
4
5
6
import java

from IfStmt ifstmt, BlockStmt blockstmt
where ifstmt.getThen() = blockstmt and
blockstmt.getNumStmt() = 0
select ifstmt, "This 'if' statement is redundant."

import java
导入适用于 Java 的标准 CodeQL 库,每个查询都以一个或多个import语句开始

from IfStmt ifstmt, BlockStmt blockstmt
定义查询的变量,声明的形式为: <type> <variable name>
IfStmtif语句
BlockStmt:语句块

where ifstmt.getThen() = blockstmt and blockstmt.getNumStmt() = 0
定义变量的条件,ifstmt.getThen() = blockstmt将这两个变量联系起来。blockstmt必须是if语句的then分支。blockstmt.getNumStmt() = 0声明该块必须为空(即不包含任何语句)
IfStmt::getThenStmt getThen(),成员谓词,获取此if语句的then分支
BlockStmt::getNumStmtint getNumStmt(),成员谓词,获取此块中直接子语句的数目

select ifstmt, “This ‘if’ statement is redundant.”
定义每个匹配项的报告内容,select用于查找不良编码实例的查询语句始终采用以下形式: select <program element>, "<alert message>"

浏览查询结果可能会发现带有else分支的if语句的例子,其中空的then分支确实起到了作用。例如:

1
2
3
4
5
6
7
if (...) {
...
} else if ("-verbose".equals(option)) {
// nothing to do - handled earlier
} else {
error("unrecognized option");
}

在这种情况下,将带有空then分支的if语句识别为多余的是错误的。一种解决方案是如果if语句有else分支,则忽略空的then分支:

1
2
3
4
5
6
7
import java

from IfStmt ifstmt, BlockStmt blockstmt
where ifstmt.getThen() = blockstmt and
blockstmt.getNumStmt() = 0 and
not exists(ifstmt.getElse())
select ifstmt, "This 'if' statement is redundant."

IfStmt::getElseStmt getElse(),成员谓词,获取此if语句的else分支

CodeQL的Java库

标准 Java 库中最重要的类可以分为以下五个类别

  1. 表示程序元素的类(例如Java的类和方法)
  2. 表示 AST 节点的类(例如语句和表达式)
  3. 表示元数据的类(例如注释和注解)
  4. 计算度量的类(例如圈复杂度和耦合度)
  5. 导航程序调用图的类

程序元素

包括包(Package)、编译单元(CompilationUnit)、类型(Type)、方法(Method)、构造函数(Constructor)和变量(Variable
它们的共同超类是Element,它提供了通用的成员谓词,用于确定程序元素的名称和检查两个元素是否相互嵌套

CallableMethodConstructor的共同超类,通过Callable引用一个可能是方法或构造函数的元素通常很方便

类型

Type类有许多子类用于表示不同类型:

  • PrimitiveType 表示一个 基本类型,即 boolean, byte, char, double, float, int, long, short之一, QL 也将 void<nulltype> 归为基本类型
  • RefType表示引用类型,包含如下子类:
    • Class Java类
    • Interface Java接口
    • EnumType Java枚举类型
    • Array Java数组类型

例如,以下查询查找程序中为int类型的所有变量:

1
2
3
4
5
6
import java

from Variable v, PrimitiveType pt
where pt = v.getType() and
pt.hasName("int")
select v

Variable::getTypeType getType(),获取变量的类型
Element::hasNamepredicate hasName(string name),如果元素具有指定的名称则该谓词成立

引用类型也根据其声明范围进行分类:

  • TopLevelType 表示在编译单元的顶层声明的引用类型
  • NestedType 是在另一个类型中声明的类型

例如,此查询查找名称与其编译单元名称不同的所有顶级类型:

1
2
3
4
5
import java

from TopLevelType tl
where tl.getName() != tl.getCompilationUnit().getName()
select tl

Element::getNamestring getName(),获取元素的名称
RefType::getCompilationUnitCompilationUnit getCompilationUnit(),获取声明此类型的编译单元
CompilationUnit::getNamestring getName(),获取编译单元的名称(不包括其扩展名)

还有几个专用的类:

最后,该库还有许多封装了常用的Java标准库类的单例类:

TypeObjectTypeCloneableTypeRuntimeTypeSerializableTypeStringTypeSystemTypeClass

例如,我们可以编写一个查询,查找直接继承Object的所有嵌套类:

1
2
3
4
5
import java

from NestedClass nc
where nc.getASupertype() instanceof TypeObject
select nc

RefType::getASupertypeRefType getASupertype(),获取此类型的直接超类

泛型

Type还有几个子类用于处理泛型类型
GenericType代表 GenericInterfaceGenericClass,它表示一个泛型类型声明,比如java.util.Map接口

1
2
3
4
5
6
7
package java.util.;

public interface Map<K, V> {
int size();

// ...
}

类型参数,如本例中的KV,由类TypeVariable表示

泛型类型的参数化实例提供了一个具体类型来实例化类型参数,如 Map<String, File>中所示。这样的类型由 ParameterizedType表示,它不同于表示其实例化来源的泛型类型GenericType。要从ParameteredType转换为相应的GenericType,可以使用谓词getSourceDeclaration。例如,我们可以使用下面的查询来查找所有java.util.Map的参数化实例:

1
2
3
4
5
6
import java

from GenericInterface map, ParameterizedType pt
where map.hasQualifiedName("java.util", "Map")
and pt.getSourceDeclaration() = map
select pt

RefType::hasQualifiedNamepredicate hasQualifiedName(string package, string type),如果在指定名称的指定包中声明了此类型,则该谓词成立。对于嵌套类型来说,名称以$作为前缀,并附加到封闭类型的名称之后,封闭类型也可能是嵌套类型
RefType::getSourceDeclarationRefType getSourceDeclaration(),获取此类型的源声明。对于泛型类型和原始类型的参数化实例,源声明是相应的泛型类型。对于泛型类型的参数化实例中声明的非参数化类型,源声明是泛型类型中的相应类型。对于其他所有类型,源声明就是类型本身

一般来说,泛型类型可能会限制类型参数可以绑定到的类型。例如,一种从字符串到数字的映射可以声明如下:

1
2
3
class StringToNumMap<N extends Number> implements Map<String, N> {
// ...
}

这意味StringToNumberMap的参数化实例只能用Number或其子类来实例化类型参数N。我们说N是一个有界类型参数,其上界是Number。在QL中,可以使用谓词GetAtypeBind查询类型变量的类型绑定。类型约束本身由类TypeBound表示,该类有一个成员谓词getType来检索变量被绑定的类型。例如,以下查询将查找所有类型绑定为Number的类型变量:

1
2
3
4
5
6
import java

from TypeVariable tv, TypeBound tb
where tb = tv.getATypeBound() and
tb.getType().hasQualifiedName("java.lang", "Number")
select tv

BoundedType::getATypeBoundTypeBound getATypeBound(),获取此类型的绑定类型(如果有的话)

为了处理不知道泛型的遗留代码,每个泛型类型都有一个没有任何类型参数的“原始”版本。在CodeQL库中,原始类型使用类RawType表示,该类有预期的子类RawClassRawInterface。同样,还有一个用于获取相应泛型类型的谓词getSourceDeclaration。例如,我们可以查询(原始)Map类型的变量:

1
2
3
4
5
6
import java

from Variable v, RawType rt
where rt = v.getType()
and rt.getSourceDeclaration().hasQualifiedName("java.util", "Map")
select v

在以下代码段中,此查询将找到m1,而不是m2:

1
2
Map m1 = new HashMap();
Map<String, String> m2 = new HashMap<String, String>();

最后,变量可以声明为通配符类型

1
Map<? extends Number, ? super Float> m;

通配符 ? extends Number? super Float 由类WildcardTypeAccess表示。与类型参数一样,通配符可能有类型界限。与类型参数不同的是通配符可以有上界(如 ? extends Number)和下界(如 ? super Float)。类WildcardTypeAccess提供了成员谓词getUpperBoundgetLowerBound,分别用于检索上界和下界

为了处理泛型方法,有类GenericMethodParameterizedMethodRawMethod,它们完全类似于用于表示泛型类型的类似命名类。

有关Java类型的更多信息,见Java中的类型

变量

Variable表示Java意义上的变量,它要么是类的成员字段(无论是静态的还是非静态的),要么是局部变量,要么是参数。所以针对这些特殊情况,有三个子类:

  • Field 表示一个Java字段
  • LocalVariableDecl 表示局部变量
  • Parameter 表示方法或构造函数的参数

抽象语法树

此类别中的类表示抽象语法树(AST)节点,即语句(类Stmt)和表达式(类Expr)。有关标准QL库中可用的表达式和语句类型的完整列表,见用于处理Java程序的抽象语法树类

ExprStmt都提供了成员谓词,用于探索程序的抽象语法树:

  • Expr.getAChildExpr 返回给定表达式的子表达式
  • Stmt.getAChild 返回直接嵌套在给定语句中的语句或表达式
  • Expr.getParentStmt.getParent 返回AST节点的父节点

例如,以下查询将查找所有父级为return语句的表达式:

1
2
3
4
5
import java

from Expr e
where e.getParent() instanceof ReturnStmt
select e

以下查询查找父级为if语句的语句(将查找程序中所有if语句的then分支和else分支):

1
2
3
4
5
import java

from Stmt s
where s.getParent() instanceof IfStmt
select s

最后,这是一个查找方法体的查询:

1
2
3
4
5
import java

from Stmt s
where s.getParent() instanceof Method
select s

正如这些示例所示,表达式的父节点并不总是表达式:它也可能是语句,例如IfStmt。类似地,语句的父节点并不总是一个语句:它也可能是一个方法或构造函数。为了解决这个问题,QL Java库提供了两个抽象类ExprParentStmtParent,前者表示可能是表达式父节点的任何节点,后者表示可能是语句父节点的任何节点

有关使用AST类的更多信息,见Java中容易溢出的比较运算

元数据

除了程序代码之外,Java程序还有几种元数据。特别是有注解Javadoc注释。由于此元数据对于增强代码分析和作为分析主题本身都很有趣,因此QL库定义了用于访问它的类

对于注解,类Annotatable是所有可注解的程序元素的超类。包括包、引用类型、字段、方法、构造函数和局部变量声明。对于每一个这样的元素,其谓词getAnAnnotation能检索该元素可能具有的任何注解。例如,以下查询查找构造函数上的所有注解:

1
2
3
4
import java

from Constructor c
select c.getAnAnnotation()

这些注解由类Annotation表示。注解只是类型为AnnotationType的表达式。例如,可以修改此查询,使其只报告废弃的构造函数

1
2
3
4
5
6
7
import java

from Constructor c, Annotation ann, AnnotationType anntp
where ann = c.getAnAnnotation() and
anntp = ann.getType() and
anntp.hasQualifiedName("java.lang", "Deprecated")
select ann

有关使用注解的更多信息,见Java中的注解

对于Javadoc,类Element有一个成员谓词getDoc,它返回一个委派的Documentable的对象,然后可以查询它附加的Javadoc注释。例如,以下查询在私有字段上查找Javadoc注释:

1
2
3
4
5
6
import java

from Field f, Javadoc jdoc
where f.isPrivate() and
jdoc = f.getDoc().getJavadoc()
select jdoc

Javadoc将整个Javadoc注释表示为JavadocElement节点树,可以使用成员谓词getAChildgetParent遍历这些节点。例如,你可以编辑查询,以便在私有字段的Javadoc注释中找到所有@author标签:

1
2
3
4
5
6
7
import java

from Field f, Javadoc jdoc, AuthorTag at
where f.isPrivate() and
jdoc = f.getDoc().getJavadoc() and
at.getParent+() = jdoc
select at

Recursion — CodeQL

有关使用Javadoc的更多信息,见Javadoc

度量

标准的QL Java库为计算Java程序元素的度量提供了广泛的支持。为了避免与度量计算相关的成员谓词过多而给代表这些元素的类造成过重的负担,这些谓词被放在委托类上

总共有六个这样的类:MetricElementMetricPackageMetricRefTypeMetricFieldMetricCallableMetricStmt。相应的元素类各自提供一个成员谓词getMetrics,可用于获取委托类的实例,然后在这个实例上进行度量计算。例如,以下查询查找圈复杂度大于40的方法:

1
2
3
4
5
6
import java

from Method m, MetricCallable mc
where mc = m.getMetrics() and
mc.getCyclomaticComplexity() > 40
select m

调用图

从Java代码生成的CodeQL数据库包含有关程序调用图的预计算信息,即给定调用在运行时可以分派给哪些方法或构造函数。

前文介绍的Callable类,它包括方法,也包括构造函数。调用表达式是使用类Call来进行抽象的,它包括方法调用、new表达式和使用thissuper的显式构造函数调用

我们可以使用谓词 Call.getCallee 来查找一个特定的调用表达式所指向的方法或构造函数。例如,以下查询查找名为println的方法的所有调用:

1
2
3
4
5
6
import java

from Call c, Method m
where m = c.getCallee() and
m.hasName("println")
select c

相反, Callable.getAReference 返回指向它的 Call 。所以我们可以使用这个查询找到从未被调用的方法或构造函数:

1
2
3
4
5
import java

from Callable c
where not exists(c.getAReference())
select c

有关可调用项和调用的更多信息,见导航调用图

Java中的数据流分析

数据流分析用于计算一个变量在程序中各个点上可能保持的值,确定这些值如何在程序中传播以及它们的使用位置

局部数据流

局部数据流是单个方法内或可调用内的数据流。局部数据流通常比全局数据流更容易、更快、更精确,并且对于许多查询来说已经足够了

使用局部数据流

局部数据流库位于DataFlow模块中,该模块定义了类Node来表示数据可以通过的任意元素。Node分为表达式节点(ExprNode)和参数节点(ParameterNode)。可以使用成员谓词asExprasParameter在数据流节点和表达式/参数之间映射:

1
2
3
4
5
6
7
8
9
class Node {
/** Gets the expression corresponding to this node, if any. */
Expr asExpr() { ... }

/** Gets the parameter corresponding to this node, if any. */
Parameter asParameter() { ... }

...
}

或者使用谓词exprNodeparameterNode

1
2
3
4
5
6
7
8
9
/**
* Gets the node corresponding to expression `e`.
*/
ExprNode exprNode(Expr e) { ... }

/**
* Gets the node corresponding to the value of parameter `p` at function entry.
*/
ParameterNode parameterNode(Parameter p) { ... }

如果存在一条从节点nodeFrom到节点nodeTo的实时数据流边,则谓词 localFlowStep(Node nodeFrom, Node nodeTo) 成立。可以通过使用+*运算符来递归地应用localFlowStep,或者通过使用预定义的递归谓词localFlow(相当于localFlowStep*

例如,可以在零个或多个局部步骤中找到从参数source到表达式sink的流:

1
DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink))
使用局部污点跟踪

局部污点跟踪通过包含非保值流步骤来扩展局部数据流。例如:

1
2
String temp = x;
String y = temp + ", " + temp;

如果x是污点字符串,那么y也是污点

局部污点跟踪库位于TaintTracking模块中。与局部数据流一样,如果存在一条从节点nodeFrom到节点nodeTo的实时污染传播边,则谓词localTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo)成立。可以使用+*运算符递归地应用谓词,或者使用预定义的递归谓词localTaint(相当于 localTaintStep*

例如,可以在零个或多个局部步骤中找到从参数source到表达式sink的污染传播:

1
TaintTracking::localTaint(DataFlow::parameterNode(source), DataFlow::exprNode(sink))
示例

此查询查找传递给新new FileReader(..)的文件名

1
2
3
4
5
6
7
import java

from Constructor fileReader, Call call
where
fileReader.getDeclaringType().hasQualifiedName("java.io", "FileReader") and
call.getCallee() = fileReader
select call.getArgument(0)

Member::getDeclaringTypeRefType getDeclaringType(),获取定义此成员的类型

但这只给出参数中的表达式,而不是可以传递给它的值。所以我们使用局部数据流来查找流入参数的所有表达式:

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

from Constructor fileReader, Call call, Expr src
where
fileReader.getDeclaringType().hasQualifiedName("java.io", "FileReader") and
call.getCallee() = fileReader and
DataFlow::localFlow(DataFlow::exprNode(src), DataFlow::exprNode(call.getArgument(0)))
select src

然后我们可以使源更加具体,例如对一个公共参数的访问。此查询查找将公共参数传递给new FileReader(..)的位置:

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

from Constructor fileReader, Call call, Parameter p
where
fileReader.getDeclaringType().hasQualifiedName("java.io", "FileReader") and
call.getCallee() = fileReader and
DataFlow::localFlow(DataFlow::parameterNode(p), DataFlow::exprNode(call.getArgument(0)))
select p

此查询查找对格式字符串没有硬编码的格式化函数的调用

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

from StringFormatMethod format, MethodAccess call, Expr formatString
where
call.getMethod() = format and
call.getArgument(format.getFormatStringIndex()) = formatString and
not exists(DataFlow::Node source, DataFlow::Node sink |
DataFlow::localFlow(source, sink) and
source.asExpr() instanceof StringLiteral and
sink.asExpr() = formatString
)
select call, "Argument to String format method isn't hard-coded."

existsexists(<variable declarations> | <formula>)。还可以写作exists(<variable declarations> | <formula 1> | <formula 2>),相当于 exists(<variable declarations> | <formula 1> and <formula 2>)。这个函数引入了一些新的变量,如果变量至少有一组值可以使主体中的公式为真,则该函数成立。例如, exists(int i | i instanceof OneTwoThree) 引入int类型的临时变量i,如果i的任何值是OneTwoThree类型,则函数成立

StringLiteralClass StringLiteral,字符串文本或文本块(Java 15特性)

练习

练习1:使用局部数据流编写一个查询,查找所有用于创建 java.net.URL的硬编码字符串(答案

全局数据流

全局数据流跟踪整个程序中的数据流,因此比局部数据流更强大。然而,全局数据流不如局部数据流精确,分析通常需要大量时间和内存

使用全局数据流

可以通过扩展类 DataFlow::Configuration来使用全局数据流库

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

class MyDataFlowConfiguration extends DataFlow::Configuration {
MyDataFlowConfiguration() { this = "MyDataFlowConfiguration" }

override predicate isSource(DataFlow::Node source) {
...
}

override predicate isSink(DataFlow::Node sink) {
...
}
}

这些谓词在配置中定义:

  • isSource—定义了数据可能从何而来
  • isSink—定义了数据可能流向的位置
  • isBarrier—可选,限制数据流
  • isAdditionalFlowStep—可选,添加额外的流程步骤

特征谓词 MyDataFlowConfiguration() 定义了配置的名称,所以"MyDataFlowConfiguration"应该是唯一的名称,例如你的类名

使用谓词 hasFlow(DataFlow::Node source, DataFlow::Node sink)执行数据流分析:

1
2
3
from MyDataFlowConfiguration dataflow, DataFlow::Node source, DataFlow::Node sink
where dataflow.hasFlow(source, sink)
select source, "Data flow to $@.", sink, sink.toString()
使用全局污点跟踪

就像局部污点跟踪是对局部数据流的跟踪一样,全局污点跟踪是对全局数据流的跟踪。也就是说,全局污点跟踪通过额外的非保值步骤扩展了全局数据流。

Difference between DataFlow::Configuration and TaintTracking::Configuration

可以通过扩展类 TaintTracking::Configuration来使用全局污点跟踪库:

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

class MyTaintTrackingConfiguration extends TaintTracking::Configuration {
MyTaintTrackingConfiguration() { this = "MyTaintTrackingConfiguration" }

override predicate isSource(DataFlow::Node source) {
...
}

override predicate isSink(DataFlow::Node sink) {
...
}
}

这些谓词在配置中定义:

  • isSource—定义了污点可能来自哪里
  • isSink—定义了污点可能流向哪里
  • isSanitizer—可选,限制污点的流动
  • isAdditionalTaintStep—可选,添加其他污点步骤

与全局数据流类似,特征谓词 MyTaintTrackingConfiguration() 定义了配置的唯一名称

污点跟踪分析使用谓词 hasFlow(DataFlow::Node source, DataFlow::Node sink)

Flow sources

数据流库包含一些预定义的流源。 RemoteFlowSource 类(在semmle.code.java.dataflow.FlowSources)中定义)表示可能由远程用户控制的数据流源,这对于查找安全问题很有用

示例

此查询显示使用远程用户输入作为数据源的污点跟踪配置

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

class MyTaintTrackingConfiguration extends TaintTracking::Configuration {
MyTaintTrackingConfiguration() {
this = "..."
}

override predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource
}

...
}
练习

练习2:编写一个查询,使用全局数据流查找用于创建 java.net.URL的所有硬编码字符串(答案

练习3:编写一个表示来自 java.lang.System.getenv(..)的流源的类(答案

练习4:使用2和3中的答案,编写一个查询,查找所有从 getenvjava.net.URL的全局数据流(答案

练习答案

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

from Constructor url, Call call, StringLiteral src
where
url.getDeclaringType().hasQualifiedName("java.net", "URL") and
call.getCallee() = url and
DataFlow::localFlow(DataFlow::exprNode(src), DataFlow::exprNode(call.getArgument(0)))
select src
练习2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import semmle.code.java.dataflow.DataFlow

class Configuration extends DataFlow::Configuration {
Configuration() {
this = "LiteralToURL Configuration"
}

override predicate isSource(DataFlow::Node source) {
source.asExpr() instanceof StringLiteral
}

override predicate isSink(DataFlow::Node sink) {
exists(Call call |
sink.asExpr() = call.getArgument(0) and
call.getCallee().(Constructor).getDeclaringType().hasQualifiedName("java.net", "URL")
)
}
}

from DataFlow::Node src, DataFlow::Node sink, Configuration config
where config.hasFlow(src, sink)
select src, "This string constructs a URL $@.", sink, "here"
练习3
1
2
3
4
5
6
7
8
9
10
import java

class GetenvSource extends MethodAccess {
GetenvSource() {
exists(Method m | m = this.getMethod() |
m.hasName("getenv") and
m.getDeclaringType() instanceof TypeSystem
)
}
}
练习4
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
import semmle.code.java.dataflow.DataFlow

class GetenvSource extends DataFlow::ExprNode {
GetenvSource() {
exists(Method m | m = this.asExpr().(MethodAccess).getMethod() |
m.hasName("getenv") and
m.getDeclaringType() instanceof TypeSystem
)
}
}

class GetenvToURLConfiguration extends DataFlow::Configuration {
GetenvToURLConfiguration() {
this = "GetenvToURLConfiguration"
}

override predicate isSource(DataFlow::Node source) {
source instanceof GetenvSource
}

override predicate isSink(DataFlow::Node sink) {
exists(Call call |
sink.asExpr() = call.getArgument(0) and
call.getCallee().(Constructor).getDeclaringType().hasQualifiedName("java.net", "URL")
)
}
}

from DataFlow::Node src, DataFlow::Node sink, GetenvToURLConfiguration config
where config.hasFlow(src, sink)
select src, "This environment variable constructs a URL $@.", sink, "here"

Java中的类型

标准CodeQL库通过Type类及其各种子类来表示Java类型

PrimitiveType类表示Java语言中内置的基本类型(如booleanint),而RefType及其子类表示引用类型,即类、接口、数组类型等。也包括来自Java标准库的类型(如Java.lang.Object)和由非库代码定义的类型

RefType类还为类层次结构建模:成员谓词getASupertypegetASubtype可以查找引用类型的直接超类和子类。例如,对于以下Java程序:

1
2
3
4
5
class A {}

interface I {}

class B extends A implements I {}

A有一个直接超类(java.lang.Object)和一个直接子类(B);接口I也是如此。而类B有两个直接超类(AI),没有直接子类

为了确定超类(包括直接超类,以及它们的超类等),我们可以使用传递闭包。例如,要在上面的示例中查找B的所有超类,我们可以使用以下查询:

1
2
3
4
5
import java

from Class B
where B.hasName("B")
select B.getASupertype+()

如果在上面的示例代码段上运行此查询,则查询将返回AIjava.lang.Object

除了类层次结构建模,RefType还提供成员谓词getAMember,用于访问类中声明的成员(即字段、构造函数和方法),以及谓词inherits(Method m),用于检查类是否声明或继承方法m

示例:查找有问题的数组强制转换

作为如何使用类层次结构API的示例,我们可以编写一个查询来查找数组的向下转型,也就是某种类型A[]转换为类型B[]的表达式eBA的(不一定是直接的)子类)

这种类型的转换是有问题的,因为向下转换数组会导致运行时异常,即使每个数组元素都可以向下转换。例如,以下代码会引发ClassCastException

1
2
Object[] o = new Object[] { "Hello", "world" };
String[] s = (String[])o;

另一方面,如果表达式e恰好计算为B[]数组,则转换将成功:

1
2
Object[] o = new String[] { "Hello", "world" };
String[] s = (String[])o;

在本文中,我们不尝试区分这两种情况。 我们的查询应该只是简单地查找从source类转换为target类的转换表达式ce

  • sourcetarget都是数组类型
  • source的元素类型是target元素类型的可传递超类
1
2
3
4
5
6
7
import java

from CastExpr ce, Array source, Array target
where source = ce.getExpr().getType() and
target = ce.getType() and
target.getElementType().(RefType).getASupertype+() = source.getElementType()
select ce, "Potentially problematic array downcast."

请注意,通过将target.getElementType() 转换为RefType,我们排除了所有元素类型为原始类型的情况,即 target是原始类型的数组:在这种情况下不会出现我们正在寻找的问题。 与 Java 不同,QL 中的强制转换永远不会失败:如果无法将表达式强制转换为所需的类型,它会简单地从查询结果中排除,这也正是我们想要的

改进

在版本5之前的旧Java代码上运行此查询,通常会返回由于使用将集合转换为T[]类型的数组的方法Collection.toArray(T[])而产生的许多误报结果

在不使用泛型的代码中,这个方法通常如下使用:

1
2
3
List l = new ArrayList();
// add some elements of type A to l
A[] as = (A[])l.toArray(new A[0]);

这段代码中,l是原始类型List,所以l.toArray返回Object[]类型,与它的参数数组的类型无关。因此从Object[]转到A[]会被我们的查询标记为有问题,尽管在运行时,这个转换永远不会出错

为了识别这些情况,我们可以创建两个CodeQL类分别用来表示Collection.toArray方法和此方法或任何重写它的方法的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** class representing java.util.Collection.toArray(T[]) */
class CollectionToArray extends Method {
CollectionToArray() {
this.getDeclaringType().hasQualifiedName("java.util", "Collection") and
this.hasName("toArray") and
this.getNumberOfParameters() = 1
}
}

/** class representing calls to java.util.Collection.toArray(T[]) */
class CollectionToArrayCall extends MethodAccess {
CollectionToArrayCall() {
exists(CollectionToArray m |
this.getMethod().getSourceDeclaration().overridesOrInstantiates*(m)
)
}

/** the call's actual return type, as determined from its argument */
Array getActualReturnType() {
result = this.getArgument(0).getType()
}
}

注意在CollectionToArrayCall的构造函数中使用了getSourceDeclarationoverridesOrInstantiates:我们希望找到对Collection.toArray方法和任何重写它的方法的调用,以及这些方法的任何参数化实例。例如,在上面的示例中,l.toArray解析为原始类型ArrayList中的toArray方法。其源声明是位于泛型类ArrayList<T>中的toArray,该类重写AbstractCollection<T>.toArray,这反过来会覆盖Collection<T>.toArray,它是Collection.toArray的一个实例化。(因为重写方法中的类型参数T属于ArrayList,并且是属于Collection的类型参数的实例)

使用这些新类,我们可以扩展查询,排除误报:

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

// Insert the class definitions from above

from CastExpr ce, Array source, Array target
where source = ce.getExpr().getType() and
target = ce.getType() and
target.getElementType().(RefType).getASupertype+() = source.getElementType() and
not ce.getExpr().(CollectionToArrayCall).getActualReturnType() = target
select ce, "Potentially problematic array downcast."

示例:查找不匹配的contains

我们现在将编写一个查询来查找查询元素的类型与集合的元素类型无关的Collection.contains的使用

例如,Apache Zookeeper以前在类QuorumPeerConfig中有一段类似于以下内容的代码:

1
2
3
4
5
6
7
Map<Object, Object> zkProp;

// ...

if (zkProp.entrySet().contains("dynamicConfigFile")){
// ...
}

由于zkProp是从ObjectObject的映射,因此zkProp.entrySet返回一个Set<Entry<Object, Object>>类型的集合。 这样的集合不可能包含String类型的元素(代码已被修复为使用zkProp.containsKey

一般来说,我们希望找到对Collection.contains的调用(或任何Collection的参数化实例中的重写了它方法),而且集合元素的类型Econtains参数的类型A是不相关的,也就是说,它们没有共同的子类

首先创建一个描述java.util.Collection的类:

1
2
3
4
5
class JavaUtilCollection extends GenericInterface {
JavaUtilCollection() {
this.hasQualifiedName("java.util", "Collection")
}
}

为了确保没有错误,可以运行一个简单的测试查询:

1
2
from JavaUtilCollection juc
select juc

这个查询应该只返回一个结果

然后创建一个描述java.util.Collection.contains的类:

1
2
3
4
5
6
class JavaUtilCollectionContains extends Method {
JavaUtilCollectionContains() {
this.getDeclaringType() instanceof JavaUtilCollection and
this.hasStringSignature("contains(Object)")
}
}

这里使用了hasStringSignature来检查以下项:

  • 该方法的名称为contains

  • 它只有一个参数

  • 参数的类型是Object

或者可以使用hasNamegetNumberOfParametersgetParameter(0).getType() instanceof TypeObject来分别实现这三项

现在我们要识别对Collection.contains的所有调用,包括任何重写它的方法,并考虑Collection的所有参数化实例以及其子类,编写如下

1
2
3
4
5
6
7
class JavaUtilCollectionContainsCall extends MethodAccess {
JavaUtilCollectionContainsCall() {
exists(JavaUtilCollectionContains jucc |
this.getMethod().getSourceDeclaration().overrides*(jucc)
)
}
}

对于每次调用contains,我们关注的是参数的类型以及调用它的集合的元素类型。 所以我们需要在类JavaUtilCollectionContainsCall中添加getArgumentTypegetCollectionElementType这两个成员谓词

前者很简单:

1
2
3
Type getArgumentType() {
result = this.getArgument(0).getType()
}

对于后者,我们将按以下步骤进行:

  • 找到被调用的contains方法的声明类型D
  • 找到D的超类S(或者D本身),且是java.util.Collection的参数化实例
  • 返回S的类型参数
1
2
3
4
5
6
7
Type getCollectionElementType() {
exists(RefType D, ParameterizedInterface S |
D = this.getMethod().getDeclaringType() and
D.hasSupertype*(S) and S.getSourceDeclaration() instanceof JavaUtilCollection and
result = S.getTypeArgument(0)
)
}

将这两个成员谓词添加到JavaUtilCollectionContainsCall中,我们还需要编写一个谓词来检查两个给定的引用类型是否具有公共子类:

1
2
3
predicate haveCommonDescendant(RefType tp1, RefType tp2) {
exists(RefType commondesc | commondesc.hasSupertype*(tp1) and commondesc.hasSupertype*(tp2))
}

现在可以编写出查询的第一个版本:

1
2
3
4
5
6
7
8
import java

// Insert the class definitions from above

from JavaUtilCollectionContainsCall juccc, Type collEltType, Type argType
where collEltType = juccc.getCollectionElementType() and argType = juccc.getArgumentType() and
not haveCommonDescendant(collEltType, argType)
select juccc, "Element type " + collEltType + " is incompatible with argument type " + argType
改进

对于很多程序来说,由于类型变量和通配符,这个查询会产生大量的误报结果:如果集合元素类型是某个类型变量 E,参数类型是String,例如 CodeQL 会认为这两者没有共同子类,我们的查询将标记调用。 排除此类误报结果的一种简单方法是简单地要求collEltTypeargType都不是TypeVariable的实例

误报的另一个来源是原始类型的自动装箱:例如,如果集合的元素类型是Integer并且参数是int类型,则谓词haveCommonDescendant将失败,因为int不是 RefType。 考虑到这一点,我们的查询应该检查collEltType不是argType的装箱类型

最后null是特殊的,因为它的类型(在 CodeQL 库中称为 <nulltype>)与每个引用类型兼容,因此我们应该将其排除在考虑之外

加上这三项改进,我们的最终查询是:

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

// Insert the class definitions from above

from JavaUtilCollectionContainsCall juccc, Type collEltType, Type argType
where collEltType = juccc.getCollectionElementType() and argType = juccc.getArgumentType() and
not haveCommonDescendant(collEltType, argType) and
not collEltType instanceof TypeVariable and not argType instanceof TypeVariable and
not collEltType = argType.(PrimitiveType).getBoxedType() and
not argType.hasName("<nulltype>")
select juccc, "Element type " + collEltType + " is incompatible with argument type " + argType

Java中容易溢出的比较运算

1
2
3
4
5
void foo(long l) {
for(int i=0; i<l; i++) {
// do something
}
}

如上代码,如果l大于2^31-1int类型的最大正值),那么这个循环将永远不会停止:i将从零开始,一直递增到2^31-1,它仍然小于l。 当它再次递增时会发生溢出,变为-2^31

初始查询

首先编写一个查找小于表达式的查询(CodeQL 类LTExpr),其中左操作数为int类型,右操作数为long类型:

1
2
3
4
5
6
import java

from LTExpr expr
where expr.getLeftOperand().getType().hasName("int") and
expr.getRightOperand().getType().hasName("long")
select expr

使用谓词getType(可用于Expr的所有子类)来确定操作数的类型。 Type定义了hasName谓词,可以用来识别原始类型intlong。 目前而言,此查询查找所有比较intlong的小于表达式,但实际上我们只对作为循环条件一部分的比较感兴趣。 此外,我们希望过滤掉任一操作数为常数的比较,因为这些不太可能是真正的错误。 修改后的查询如下所示:

1
2
3
4
5
6
7
8
import java

from LTExpr expr
where expr.getLeftOperand().getType().hasName("int") and
expr.getRightOperand().getType().hasName("long") and
exists(LoopStmt l | l.getCondition().getAChildExpr*() = expr) and
not expr.getAnOperand().isCompileTimeConstant()
select expr

LoopStmt类是所有循环的通用超类,当然也包括上面示例中的for循环。 虽然不同种类的循环有不同的语法,但它们都有一个循环条件,可以通过谓词getCondition获取。 我们使用应用于getAChildExpr谓词的自反传递闭包运算符*来表达expr应该嵌套在循环条件内的要求。 特别是,它可以是循环条件本身

where子句中的最后一个连词利用了谓词可以返回多个值这一特性。 比如getAnOperand可以返回expr的任一操作数,因此如果至少有一个操作数是常量,则expr.getAnOperand().isCompileTimeConstant()成立。 否定这个条件意味着查询只会找到两个操作数都不是常量的表达式

完善查询

当然,intlong之间的比较并不是唯一有问题的情况:窄类型和宽类型之间的任何小于比较都可能是可疑的,小于等于、大于和大于等于与小于比较一样有问题

为了比较类型的范围,我们定义了一个谓词,它返回给定整数类型的宽度(以位为单位):

1
2
3
4
5
6
7
int width(PrimitiveType pt) {
(pt.hasName("byte") and result=8) or
(pt.hasName("short") and result=16) or
(pt.hasName("char") and result=16) or
(pt.hasName("int") and result=32) or
(pt.hasName("long") and result=64)
}

现在我们想将我们的查询推广应用于任何类型宽度较小的一端小于较大的一端的比较,引入一个抽象类对其进行建模:

1
2
3
4
abstract class OverflowProneComparison extends ComparisonExpr {
Expr getLesserOperand() { none() }
Expr getGreaterOperand() { none() }
}

这个类有两个具体的子类:一个用于<=<比较,另一个用于>=>比较。在这两种情况下,我们实现构造函数的方式都是只匹配我们想要的表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
class LTOverflowProneComparison extends OverflowProneComparison {
LTOverflowProneComparison() {
(this instanceof LEExpr or this instanceof LTExpr) and
width(this.getLeftOperand().getType()) < width(this.getRightOperand().getType())
}
}

class GTOverflowProneComparison extends OverflowProneComparison {
GTOverflowProneComparison() {
(this instanceof GEExpr or this instanceof GTExpr) and
width(this.getRightOperand().getType()) < width(this.getLeftOperand().getType())
}
}

现在,我们利用这些新类重写查询:

1
2
3
4
5
6
7
8
import Java

// Insert the class definitions from above

from OverflowProneComparison expr
where exists(LoopStmt l | l.getCondition().getAChildExpr*() = expr) and
not expr.getAnOperand().isCompileTimeConstant()
select expr

导航调用图

CodeQL 具有用于识别调用其他代码的代码以及可以从其他地方调用的代码的类。 例如可以用来找到从未使用过的方法

调用图类

Java 的 CodeQL 库提供了两个抽象类来表示程序的调用图:CallableCall。 前者简单来说就是MethodConstructor的共同超类,后者是MethodAccessClassInstanceExpressionThisConstructorInvocationStmtSuperConstructorInvocationStmt的公共超类。 简单地说,Callable 是可以调用的东西,Call 是调用 Callable 的东西

例如,在以下程序中,所有可调用项和调用都已添加注释:

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
class Super {
int x;

// callable
public Super() {
this(23); // call
}

// callable
public Super(int x) {
this.x = x;
}

// callable
public int getX() {
return x;
}
}

class Sub extends Super {
// callable
public Sub(int x) {
super(x+19); // call
}

// callable
public int getX() {
return x-19;
}
}

class Client {
// callable
public static void main(String[] args) {
Super s = new Sub(42); // call
s.getX(); // call
}
}

Call类提供了两个导航调用图的谓词:

  • getCallee 返回此调用(静态)解析为的 Callable; 请注意,对于实例(即非静态)方法的调用,在运行时调用的实际方法可能是重写此方法的其他方法
  • getCaller获取此调用的可调用对象

例如,在我们的示例中,Client.main 中第二次调用, getCallee 将返回 Super.getX。 但是,在运行时,这个调用实际上会调用 Sub.getX

Callable定义了大量成员谓词;就我们的目的而言,最重要的两个方面是:

  • calls(Callable target):如果此可调用对象调用target则成立
  • polyCalls(Callable target):如果此可调用对象可以调用指定的可调用对象则成立

在我们的示例中,Client.main calls构造函数 Sub(int)Super.getX方法; 此外,它 polyCalls Sub.getX方法

示例:查找未使用的方法

我们可以使用 Callable 类编写一个查询来查找未被任何其他方法调用的方法:

1
2
3
4
5
import java

from Callable callee
where not exists(Callable caller | caller.polyCalls(callee))
select callee

在这里使用 polyCalls 而不是calls:我们希望合理地确定被调用者没有被调用,无论是直接调用还是通过覆盖调用

在一般的 Java 项目上运行此查询会在 Java 标准库中产生大量命中。 因为没有一个客户端程序使用标准库的所有方法。 更一般地说,我们可能希望从编译的库中排除方法和构造函数。 我们可以使用谓词 fromSource 来检查编译单元是否是源文件,并细化我们的查询:

1
2
3
4
5
6
import java

from Callable callee
where not exists(Callable caller | caller.polyCalls(callee)) and
callee.getCompilationUnit().fromSource()
select callee, "Not called."

我们可能还会注意到几个名称有点奇怪的未使用方法 <clinit>:它们是类初始化器; 虽然它们没有在代码中的任何地方显式调用,但只要加载上下文的类,它们就会被隐式调用。 因此,将它们从我们的查询中排除是有意义的。 当我们这样做时,我们还可以排除终结器,它们同样被隐式调用:

1
2
3
4
5
6
7
import java

from Callable callee
where not exists(Callable caller | caller.polyCalls(callee)) and
callee.getCompilationUnit().fromSource() and
not callee.hasName("<clinit>") and not callee.hasName("finalize")
select callee, "Not called."

我们可能还想从查询中排除公共方法,因为它们可能是外部API入口点:

1
2
3
4
5
6
7
8
import java

from Callable callee
where not exists(Callable caller | caller.polyCalls(callee)) and
callee.getCompilationUnit().fromSource() and
not callee.hasName("<clinit>") and not callee.hasName("finalize") and
not callee.isPublic()
select callee, "Not called."

另一个特殊情况是非公共默认构造函数:例如,在单例模式中,为类提供了私有的空默认构造函数以防止它被实例化。 由于此类构造函数的目的是不调用它们,因此不应标记它们:

1
2
3
4
5
6
7
8
9
import java

from Callable callee
where not exists(Callable caller | caller.polyCalls(callee)) and
callee.getCompilationUnit().fromSource() and
not callee.hasName("<clinit>") and not callee.hasName("finalize") and
not callee.isPublic() and
not callee.(Constructor).getNumberOfParameters() = 0
select callee, "Not called."

最后,在许多 Java 项目中,存在通过反射间接调用的方法。 因此,虽然没有调用这些方法的调用,但它们实际上已被使用。 通常很难识别这些方法。 然而,一个非常常见的特殊情况是 JUnit 测试方法,它由测试运行程序反射调用。 Java 的 CodeQL 库支持识别 JUnit 和其他测试框架的测试类,我们可以使用它们来过滤掉这些类中定义的方法:

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

from Callable callee
where not exists(Callable caller | caller.polyCalls(callee)) and
callee.getCompilationUnit().fromSource() and
not callee.hasName("<clinit>") and not callee.hasName("finalize") and
not callee.isPublic() and
not callee.(Constructor).getNumberOfParameters() = 0 and
not callee.getDeclaringType() instanceof TestClass
select callee, "Not called."

Java中的注解

Java项目的CodeQL数据库包含所有附加到程序元素的注解信息。

注解由以下CodeQL类表示:

  • Annotatable 表示所有可能附加注解的实体 (包,引用类型,字段,方法,局部变量)
  • AnnotationType 标识Java注解类型,例如 java.lang.Override;注解类型是接口
  • AnnotationElement 表示注解元素,即注解类型的成员
  • Annotation 标识注解,例如 @Override;可以通过成员谓词getValue访问注解值

例如,Java标准库定义了一个注解SuppressWarnings,指示编译器不要发出某些类型的警告:

1
2
3
4
5
package java.lang;

public @interface SuppressWarnings {
String[] value;
}

SuppressWarnings表示为AnnotationTypevalue是其唯一的AnnotationElement

1
2
3
4
5
class A {
@SuppressWarnings("rawtypes")
public A(java.util.List rawlist) {
}
}

表达式 @SuppressWarnings("rawtypes") 表示为 Annotation。字符串 "rawtypes" 用于初始化注解元素 value,其值可以通过getValue谓词从注解中提取

可以编写如下查询查找所有附加到构造函数的注解 @SuppressWarnings , 并返回注解本身及其value元素的值:

1
2
3
4
5
6
7
import java

from Constructor c, Annotation ann, AnnotationType anntp
where ann = c.getAnAnnotation() and
anntp = ann.getType() and
anntp.hasQualifiedName("java.lang", "SuppressWarnings")
select ann, ann.getValue("value")

另一个示例是查找所有只有一个注释元素的注释类型,该元素具有名称value

1
2
3
4
5
6
7
8
import java

from AnnotationType anntp
where forex(AnnotationElement elt |
elt = anntp.getAnAnnotationElement() |
elt.getName() = "value"
)
select anntp

Javadoc

为了访问与程序元素相关联的Javadoc,我们使用Element类的成员谓词getDoc,它返回一个Documentable。类Documentable反过来提供了一个成员谓词getJavadoc来检索附加到相关元素(如果有的话)的Javadoc

Javadoc注释由类Javadoc表示,该类提供了作为JavadocElement节点树的注释视图。每个JavadocElement要么是一个JavadocTag,表示一个标签,要么是一个JavadocText,表示一段自由格式的文本

Javadoc类最重要的成员谓词:

  • getAChild - 获取树表示中的顶级JavadocElement节点
  • getVersion - 返回@version标签的值(如果有)
  • getAuthor - 返回@author标签的值(如果有)

例如,以下查询查找所有同时具有@author标签和@version标签的类,并返回此信息:

1
2
3
4
5
6
7
import java

from Class c, Javadoc jdoc, string author, string version
where jdoc = c.getDoc().getJavadoc() and
author = jdoc.getAuthor() and
version = jdoc.getVersion()
select c, author, version

JavadocElement定义成员谓词getAChildgetParent,以在元素树上下导航。它还提供了一个谓词getTagName来返回标签的名称,以及一个谓词getText来访问与标签关联的文本

我们可以使用这个API代替getAuthorgetVersion重写上面的查询:

1
2
3
4
5
6
7
import java

from Class c, Javadoc jdoc, JavadocTag authorTag, JavadocTag versionTag
where jdoc = c.getDoc().getJavadoc() and
authorTag.getTagName() = "@author" and authorTag.getParent() = jdoc and
versionTag.getTagName() = "@version" and versionTag.getParent() = jdoc
select c, authorTag.getText(), versionTag.getText()

JavadocTag有几个子类,代表特定类型的Javadoc标签:

  • ParamTag表示@param 标签;成员谓词getParamName返回要记录的参数的名称
  • ThrowsTag表示@throws标签;成员谓词getExceptionName返回正在记录的异常的名称
  • AuthorTag表示@author标签;成员谓词getAuthorName返回作者的名称

参考文档

CodeQL从入门到放弃

https://codeql.github.com/docs/