CodeQL 文档

分析 Java 和 Kotlin 中的数据流

您可以使用 CodeQL 来跟踪数据在 Java/Kotlin 程序中流向其使用位置的过程。

注意

CodeQL 对 Kotlin 的分析目前处于测试阶段。在测试阶段,对 Kotlin 代码的分析以及相关的文档不会像其他语言一样全面。

编写针对 Kotlin 与 Java 分析的 CodeQL 查询

通常情况下,您可以使用相同的类来编写针对 Kotlin 和 Java 的查询。您使用相同的库,例如 DataFlow、TaintTracking 或 SSA,以及相同的类,例如 MethodAccessClass,用于这两种语言。当您想要访问 Kotlin 特定的元素(例如 WhenExpr)时,您需要使用 Kotlin 特定的 CodeQL 类。

但是,在某些情况下,编写针对 Kotlin 的查询与编写针对 Java 的查询相比,可能会产生出乎意料的结果,因为 CodeQL 使用 Kotlin 源代码的 JVM 字节码表示来进行处理。

在您对 Java 中不存在的代码元素进行建模时要小心,例如 NotNullExpr (expr!!),因为它们可能会以意想不到的方式与常见谓词交互。例如,在以下 Kotlin 代码中,MethodAccess.getQualifier() 返回的是 NotNullExpr,而不是 VarAccess

someVar!!.someMethodCall()

在这种特定情况下,您可以使用谓词 Expr.getUnderlyingExpr()。这会直接转到底层的 VarAccess,以产生与 Java 中更相似的行为。

可空元素 (?) 也会产生意想不到的行为。为了避免 NullPointerException,当 expr 可空时,Kotlin 可能会将类似 expr.toString() 的调用内联到 String.valueOf(expr) 中。请确保您在提取的代码周围编写 CodeQL,因为该代码可能与代码库中编写的代码不完全匹配。

另一个例子是,Kotlin 中的 if-else 表达式在 CodeQL 中被翻译成 WhenExprs,而不是在 Java 中更常见的 IfStmt

总的来说,您可以使用 AST 来调试这些问题(您可以从 Visual Studio Code 的 CodeQL 扩展中使用 CodeQL: View AST 命令,或运行 PrintAst.ql 查询),并准确地了解 CodeQL 从您的代码中提取了什么内容。

关于本文

本文介绍了如何在 CodeQL 库中实现对 Java/Kotlin 的数据流分析,并包含一些示例来帮助您编写自己的数据流查询。以下部分将介绍如何使用这些库进行本地数据流、全局数据流和污点追踪。

有关数据流建模的更一般性介绍,请参阅“关于数据流分析。”

注意

从 CodeQL 2.13.0 开始,这里描述的新型模块化数据流 API 与之前的库并存。有关库变更方式以及如何将任何现有查询迁移到模块化 API 的信息,请参阅 用于 CodeQL 查询编写的新的数据流 API

本地数据流

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

使用本地数据流

要使用数据流库,您需要以下导入

import semmle.code.java.dataflow.DataFlow

DataFlow 模块定义了类 Node,表示任何数据可以流经的元素。Node 被分为表达式节点 (ExprNode) 和参数节点 (ParameterNode)。您可以使用成员谓词 asExprasParameter 在数据流节点和表达式/参数之间进行映射

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

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

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

例如,您可以查找从参数 source 到表达式 sink 的零步或多步本地数据流

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

使用本地污点追踪

本地污点追踪通过包含非值保留的流步骤来扩展本地数据流。例如

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

如果 x 是一个被污染的字符串,那么 y 也会被污染。

要使用污点追踪库,您需要以下导入

import semmle.code.java.dataflow.TaintTracking

与本地数据流类似,谓词 localTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) 表示如果从节点 nodeFrom 到节点 nodeTo 存在一条直接的污点传播边。您可以使用 +* 运算符递归地应用该谓词,或者使用预定义的递归谓词 localTaint,它等效于 localTaintStep*

例如,您可以查找从参数 source 到表达式 sink 的零步或多步污点传播

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

此查询查找格式化函数的调用,其中格式字符串不是硬编码的。

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

练习

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

全局数据流

全局数据流跟踪整个程序中的数据流,因此比本地数据流更强大。但是,全局数据流不如本地数据流精确,并且分析通常需要更多时间和内存才能执行。

注意

您可以在 CodeQL 中通过创建路径查询来对数据流路径进行建模。要查看由 VS Code 的 CodeQL 中的路径查询生成的数据流路径,您需要确保它具有正确的元数据和 select 子句。有关更多信息,请参阅 创建路径查询

使用全局数据流

您通过实现签名 DataFlow::ConfigSig 并应用模块 DataFlow::Global<ConfigSig> 来使用全局数据流库

import semmle.code.java.dataflow.DataFlow

module MyFlowConfiguration implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) {
    ...
  }

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

module MyFlow = DataFlow::Global<MyFlowConfiguration>;

这些谓词在配置中定义

  • isSource—定义数据可能流出的位置
  • isSink—定义数据可能流入的位置
  • isBarrier—可选,限制数据流
  • isAdditionalFlowStep—可选,添加额外的流步骤

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

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

使用全局污点追踪

全局污点追踪对于全局数据流来说,就像本地污点追踪对于本地数据流一样。也就是说,全局污点追踪通过添加额外的非值保留步骤来扩展全局数据流。您通过将模块 TaintTracking::Global<ConfigSig> 应用到您的配置,而不是 DataFlow::Global<ConfigSig> 来使用全局污点追踪库

import semmle.code.java.dataflow.TaintTracking

module MyFlowConfiguration implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) {
    ...
  }

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

module MyFlow = TaintTracking::Global<MyFlowConfiguration>;

所得模块的签名与从 DataFlow::Global<ConfigSig> 获得的签名相同。

流源

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

示例

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

import java
import semmle.code.java.dataflow.FlowSources

module MyFlowConfiguration implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) {
    source instanceof RemoteFlowSource
  }

  ...
}

module MyTaintFlow = TaintTracking::Global<MyFlowConfiguration>;

练习

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

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

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

答案

练习 1

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

import semmle.code.java.dataflow.DataFlow

module LiteralToURLConfig implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) {
    source.asExpr() instanceof StringLiteral
  }

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

module LiteralToURLFlow = DataFlow::Global<LiteralToURLConfig>;

from DataFlow::Node src, DataFlow::Node sink
where LiteralToURLFlow::flow(src, sink)
select src, "This string constructs a URL $@.", sink, "here"

练习 3

import java

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

练习 4

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

module GetenvToURLConfig implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) {
    source instanceof GetenvSource
  }

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

module GetenvToURLFlow = DataFlow::Global<GetenvToURLConfig>;

from DataFlow::Node src, DataFlow::Node sink
where GetenvToURLFlow::flow(src, sink)
select src, "This environment variable constructs a URL $@.", sink, "here"
  • ©GitHub, Inc.
  • 条款
  • 隐私