返回介绍

4 CodeQL for Java

发布于 2024-09-12 23:59:25 字数 21182 浏览 0 评论 0 收藏 0

4.1 基本查询

对 if 语句中的冗余代码进行搜索,例如空的 then 分支,示例代码如下:

if (error) {}

编写 查询语句 如下:

# 引入 Java 标准查询库
import java

# 定义查询变量,声明 IfStmt 变量代表 if 语句
# 声明 BlockStmt 变量代表 then 代码块
from IfStmt ifstmt, BlockStmt block
# 定义查询的限制条件
where ifstmt.getThen() = block and
  block.getNumStmt() = 0
# 将结果返回到控制台 select <program element>, "<alert message>"
select ifstmt, "This 'if' statement is redundant."

查询优化

编写 QL 代码的过程是一个迭代的过程,在最初的查询结果中可能会出现一些「非预期」的结果,因此需要通过不断修改,来完善 QL 查询代码。

在如下示例代码中,空的 else if 分支的确有着自己的用途,因此优化查询:当 if 语句中具有 else 分支时,认为空分支有自己的作用,忽略空分支。

if (...) {
  ...
} else if ("-verbose".equals(option)) {
  // nothing to do - handled earlier
} else {
  error("unrecognized option");
}

查询语句优化:

where ifstmt.getThen() = block and
  block.getNumStmt() = 0 and
  not exists(ifstmt.getElse())

4.2 CodeQL 中的 Java 库

在分析一个 Java 程序时,可以利用 CodeQL 库中提供的大量类的集合。该库用于分析从 Java 项目中提取的 CodeQL 数据库。这个库中的类以面向对象的形式呈现数据库中的数据,并提供抽象和谓词来完成常见的分析任务。

这个库是作为一组 QL 模块实现的,也就是扩展名为 .qll 的文件。 java.qll 模块导入了所有的核心 Java 库模块,因此可以通过在查询中使用 import java 来使用该库。

4.2.1 五大类库

  • Program Elements ,程序元素,例如类和方法
  • AST nodes ,抽象树节点,例如语句和表达式
  • Metadata ,元数据,例如注解和注释
  • metrics ,计算指标,例如循环复杂度
  • Call Gragh ,调用图

4.2.2 程序元素

这些类包括:包(Package)、编译单元(CompilationUnit)、类型(Type)、方法(Method)、构造函数(Constructor)和变量(Variable)。

它们共同的超类是 Element,它提供了常用的成员谓词,用于确定程序元素的名称和检查两个元素是否相互嵌套。

因此可以方便的引用一个方法或构造函数的元素。此外, Callable 类是 MethodConstructor 的共同超类,可以用于此目的。

类型:Types

类 Type 有许多子类,用于表示不同种类的类型。

  • PrimitiveType 表示原始类型,即 boolean , byte , char , double , float , int , long , short ;QL 也将 voidnulltype 归为原始类型。
  • RefType 是非原始类型,它又有几个子类。
    • Class
    • interface
    • enum
    • Array

例如,要查询程序中所有的 int 类型的变量:

import java

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

引用类型也是根据它们的声明范围来分类的。

  • TopLevelType 代表在编译单元(一个 .java 文件) 的顶层声明的类。
  • NestedType 是一个在另一个类型内声明的类型。
    • LoadClass :在成员方法或构造方法中声明的类
    • AnonymousClass :匿名类

例如,如下 查询 可以找到所有名称与其编译单元不一致的顶层类型。

import java

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

最后,该库还有一些单例子类,如: TypeObjectTypeCloneableTypeRuntimeTypeSerializable 、TypeString、 TypeSystemTypeClass 。每个 CodeQL 类都代表其名称所暗示的标准 Java 类。

写一个找到所有直接继承 Object 的嵌套类的 查询

import java

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

泛型:Generics

GenericTypeGenericInterfaceGenericClass 。它代表了一个泛型型声明,如 Java 标准库中的接口 java.util.Map :

package java.util.;

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

类型参数,如本例中的 K 和 V,由 TypeVariable 类表示。

一个泛型的参数化实例提供了一个具体实现该类型的参数,如 Map<String, File> 。这样的类型由 ParameterizedType 表示,它与 GenericType 不同。要从 ParameterizedType 到其相应的 GenericType ,可以使用谓词 getSourceDeclaration

例如,我们可以使用下面的 查询 来找到 java.util.Map 的所有参数化实例。

import java

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

一般来说,泛型需要限制类型参数可以与哪些类型绑定。例如,一个从字符串到数字的映射类型可以被声明如下:

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

这意味着 StringToNumberMap 的参数化实例只能使用 Number 或它的一个子类型来实例化类型参数 N,而不能用其它类,如说 File 。我们说 N 是一个有界的类型参数, Number 是它的上界。在 QL 中,一个类型变量可以用谓词 getATypeBound 来查询它的类型边界。类型边界本身由 TypeBound 类表示,它有一个成员谓词 getType 来检索变量被约束的类型。

如下的 查询 找到所有以 Number 类型为界限的变量:

import java

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

为了处理那些在泛型出现之前的遗留代码,每个泛型都有一个没有任何类型参数的 「原始」版本。在 CodeQL 库中,原始类型用 RawType 类表示,它有预期的子类 RawClassRawInterface 。同样,有一个谓词 getSourceDeclaration 用于获得相应的通用类型。如下的 查询 可以找到(原始)类型 Map 的变量。实际上,现在仍然有许多项目在使用原始类型的 Map。

import java

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

上述的查询语句,针对如下代码片段,只能查找到 m1 而不能查找到 m2。

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

变量:Variable

Variable 表示 Java 中的变量,它可以是一个类的成员字段(无论是否静态),也可以是一个局部变量,或者是函数的参数。因此,有三个子类来满足这些特殊情况的需要。

  • Field :字段
  • LocalVariableDecl :本地变量。
  • Parameter :方法或构造函数的参数。

4.2.3 抽象语法树:Abstract Syntax Tree

该类中包含了抽象语法树的节点,也就是语句(QL 中的类 Stmt )和表达式(QL 中的类 Expr )。关于标准 QL 库中可用的表达式和语句类型的完整列表,可以参考该 链接

ExprStmt 都提供了成员谓词来获取程序的抽象语法树:

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

下面的 查询 可以找到所有父类为返回语句的表达式。

import java

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

因此,程序中如果包含: return x + y 子表达式,QL 的查询结果将会返回: x + y

下面的 查询 可以找到某个表达式的父级为 if 语句:

import java

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

这个查询将找到程序中所有 if 语句的 then 分支和 else 分支。

最后,一个 查询 的例子,可以找到方法体。

import java

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

如上的这些例子可知,表达式的父节点并不总是表达式:它也可能是一个语句,例如 IfStmt 。同样,语句的父节点也不总是一个语句:它也可能是一个方法或构造函数。为了抓住这一点,QL Java 库提供了两个抽象类 ExprParentStmtParent ,前者代表可能是表达式的父节点的任何节点,后者代表可能是语句的父节点的任何节点。

4.2.4 元数据:Metadata

除了 Java 程序代码本身之外,Java 程序还有几种元数据。其中包括有注解(Annotations) 和 Javadoc 注释。由于这些元数据对于加强代码分析或者是作为分析目标本身都很有用处,因此,QL 库定义了用于访问这些元数据的类。

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

import java

from Constructor c
select c.getAnAnnotation()

LGTM.com 的示例结果中都使用了注解。其中的一些返回结果,它们被用来取消警告和标记代码为废弃的。这些注释是由类 Annotation 表示的。一个注释是一个表达式,其类型是 AnnotationType 。例如,通过修改 查询 ,使其只返回废弃的构造函数。

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

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

import java

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

Javadoc 类将整个 Javadoc 注释转换为一棵 JavadocElement 节点的树,可以使用成员谓词 getAChildgetParent 对其进行遍历。例如,编辑 查询 ,使其找到 Javadoc 注释中所有关于私有字段的 @author 标签。

import java

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

4.2.5 指标:Metrics

标准的 QL Java 库提供了对 Java 程序元素计算度量的广泛支持。为了避免代表这些元素的类因有太多与度量计算有关的成员谓词而负担过重,这些谓词在委托类中可用。

总共有六个这样的 QL 类。 MetricElementMetricPackageMetricRefTypeMetricFieldMetricCallableMetricStmt 。相应的元素类都提供了一个成员谓词 getMetrics ,可以用来获取委托类的一个实例,然后对其进行度量计算。

查询 举例:查询循环复杂度大于 40 的方法。

import java

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

4.2.6 调用图:Call graph

从 Java 程序代码中生成的 CodeQL 数据库包括关于程序调用图的预计算信息,也就是说,一个给定的调用在运行时可能分配 (dispatch) 给哪些方法或构造函数。

上面介绍的 Callable 类,包括方法和构造函数。调用表达式是用类 Call 来抽象的,包括方法调用、 new 表达式和使用 thissuper 的显式构造函数调用。

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

import java

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

此外, Callable.getAReference 会返回一个指代它的 Call。因此,可以用这个查询找到那些从未被调用的方法和构造函数。代码 示例 :查询未被引用过的调用:

import java

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

4.3 数据流分析

本小节描述了如何在 CodeQL 中的 Java 库进行数据流分析的,并包括几个使用数据流查询的例子。下面几节描述了如何使用库进行本地数据流、全局数据流和污点跟踪。

4.3.1 本地数据流

使用本地数据流

本地数据流的作用域限定在一个方法或调用内。本地数据流相比全局数据流更容易,更快速,更准确。本地数据流相关的库位于 DataFlow 模块中,需要手动导入。

import semmle.code.java.dataflow.DataFlow

数据流节点(Node)可以分为 ExprNodeParamterNodeDataFlow::Node 的两个谓词,可以将数据流节点转 ExprParameter 的形式。

class Node {
  // 获取数据流节点 Expr 的形式
  Expr asExpr() { ... }
	
  // 获取数据流节点 Parameter 的形式
  Parameter asParameter() { ... }
}

DataFlow 的两个谓词,可以将 ExprParameter 转数据流节点的形式:

// 获取 Expr 对应的数据流节点形式
ExprNode DataFlow::exprNode(Expr e)

// 获取 Parameter 对应的数据流节点形式
ParameterNode DataFlow::parameter(Parameter p)

如果存在从节点 nodeFrom 到节点 nodeTo 的直接数据流边,谓词 localFlowStep(Node nodeFrom, Node nodeTo) 成立。可以通过使用 +* 运算符递归地应用该谓词,或者使用预定义的递归谓词 localFlow ,它等同于 localFlowStep*

例如,可以通过 DataFlow 的谓词 localFlowStep 限定从 nodeFrom 流向 nodeTo 的数据流。

DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink))

使用本地污点追踪

如下示例代码中,如果 x 被定义为污点,那么 y 也将是污点。

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

本地污点跟踪库在 TaintTracking 模块中。与本地数据流一样,可以使用 TaintTracking 的谓词 localTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) 限定从 nodeFrom 流向 nodeTo 的数据流。

可以通过使用 +* 操作符递归地应用该谓词,或者使用预定义的递归谓词 localTaint ,它等同于 localTaintStep*

TaintTracking::localTaint(DataFlow::parameterNode(source), DataFlow::exprNode(sink))

示例

如下查询可以找到传递给 new FileReader(..) 的文件名。

import java

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

但是,该查询的结果只给出了参数中的表达式,而不是可以传递给它的值。所以使用本地数据流来找到所有流入参数的表达式。

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(...) 中。

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

4.3.2 全局数据流

全局数据流比本地数据流更强大,但是执行时也更消耗时间与内存。

使用全局数据流

需要继承 DataFlow::Configuration

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) {
    ...
  }
}

Configuration 内置的几个谓词

  • isSource :定义数据流的来源
  • isSink :定义数据流的终点
  • isBarrier :可选,限制数据流
  • isAdditionalFlowStep :可选,限制数据流的步数

然后通过调用 Configuration 的谓词 hasFlow(DataFlow::Node source, DataFlow::Node sink) 来执行数据流分析。

from MyDataFlowConfiguration dataflow, DataFlow::Node source, DataFlow::Node sink
where dataflow.hasFlow(source, sink)
select source, "Data flow to $@.", sink, sink.toString()

使用全局污点追踪

需要继承 TaintTracking::Configuration

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) {
    ...
  }
}

Configuration 内置的几个谓词:

  • isSource :污染源
  • isSink :污染汇聚点
  • isSanitizer :过滤器
  • isAdditionalTaintStep :数据流拼接

全局污点追踪的执行与全局数据流的执行相似,也是通过调用 ConfigurationhasFlow 谓词。

4.4 Java 类型

4.4.1 关于 Java 中的类型

此小结是 https://codeql.github.com/docs/codeql-language-guides/types-in-java/ 对 内容的进一步阐述

CodeQL 中提供了 Type 类以及子类,来表示 Java 中的各种类型。

正如前文所述,PrimitiveType 类用来表示 Java 中所有的主数据类型,例如 boolean、int。RefType 类用来表示 Java 中所有的引用类型,例如 Array、字符串等。

RefType 类提供了两个成员谓词 getASupertypegetASubtype 来查找该引用类型对应的超类和子类。

class A {}

interface I {}

class B extends A implements I {}

如上所示:A 的超类是 java.lang.Object ,子类是 B;B 的超类有 A 和 I,没有子类。

以获取 B 类所有的超类为例:

import java

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

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

4.4.2 示例

寻找可能存在问题的数据转换

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

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

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

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

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

在本教程中,我们不会尝试区分这两种情况。 我们的查询应该仅仅查找从某个类型源转换为另一个类型目标的转换表达式 ce ,例如:

  • 源和目标都是数组类型。
  • 源的元素类型是目标的元素类型的递归超类。

转换为查询语句:

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."

Array 类的成员谓词 getElementType 可以获得该数组中元素的数据类型,result 是 Type 类型的

Type Array::getElementType()

在第 6 行中,将 Type 类型向下转换为 RefType 类型,然后调用 RefType 类的成员谓词 getASupertype

target.getElementType().(RefType).getASupertype+()

标识方法

Java 支持重载,可以通过限制形参的数据类型来标识固定的某个方法:

标识 java.util.Collection.contains(Object) 方法

class JavaUtilCollection extends GenericInterface {
    JavaUtilCollection() {
        this.hasQualifiedName("java.util", "Collection")
    }
}

class JavaUtilCollectionContains extends Method {
    JavaUtilCollectionContains() {
        this.getDeclaringType() instanceof JavaUtilCollection and
        this.hasStringSignature("contains(Object)")
    }
}

如上所示的 hasStringSignature 谓词有如下作用:

  1. 限制方法名为 contains,可以使用谓词 hasName 来替代
  2. 限制参数为 1 个,可以使用谓词 getNumberOfParameters 来替代
  3. 限制参数的类型是 Object 类型,可以使用如下方式来替代
getParameter(0).getType() instanceof TypeObject.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文