CodeQL 文档

使用流标签进行精确数据流分析

您可以将流标签与流分析跟踪的每个值关联起来,以确定流是否包含潜在的漏洞。

概述

您可以使用“分析 JavaScript 和 TypeScript 中的数据流”中描述的基本跨过程数据流分析和污染跟踪来检查数据流图中是否存在从某个源节点到接收节点的路径,该路径不经过任何清理节点。另一种思考方式是,它静态地模拟数据在程序中的流动,并将一个标记与每个数据值关联起来,告诉我们它是否可能来自源节点。

在某些情况下,您可能希望跟踪有关数据值的更详细的信息。这可以通过将流标签与数据值关联来实现,如本教程所示。我们将首先讨论流标签背后的总体思路,然后展示如何在实践中使用它们。最后,我们将概述相关 API 并提供一些使用流标签的标准查询的指针。

基本数据流分析的局限性

在许多应用程序中,我们感兴趣的不仅仅是跨过程数据流分析提供的可达性信息。

例如,当跟踪源自不受信任输入的对象值时,我们可能希望记住整个对象是否被污染,或者只有它的一部分被污染。前者发生在例如将用户控制的字符串解析为 JSON 时,这意味着整个生成的都是被污染的。后者的一个典型例子是将被污染的值分配给对象的属性,这只会污染该属性,而不会污染对象的其余部分。

读取完全被污染的对象的属性会生成被污染的值,而读取部分被污染的对象的属性则不会。另一方面,将被污染的对象(即使是部分被污染的对象)进行 JSON 编码并将其包含在 HTML 文档中是不安全的。

另一个需要更细粒度信息来跟踪被污染值的示例是跟踪部分清理。例如,在将用户控制的字符串解释为文件系统路径之前,我们通常希望确保它既不是绝对路径(可以引用文件系统上的任何文件),也不是包含 .. 组件的相对路径(仍然可以引用任何文件)。通常,检查这两个属性需要进行两个单独的检查。这两个检查加在一起应该算作清理,但每个单独的检查本身不足以使字符串能够安全用作路径。为了精确地处理这种情况,我们希望将两条信息与每个被污染的值关联起来,即它是否可能是绝对路径,以及它是否可能包含 .. 组件。不受信任的用户输入最初都设置了这两个位,单独的检查会关闭单个位,如果至少设置了一个位的某个值被解释为路径,则会标记潜在的漏洞。

使用流标签

您可以通过将一组 流标签(有时也称为 污染类型)与分析跟踪的每个值关联来处理这些情况以及类似的情况。保持值不变的数据流步骤(例如,从写入变量到读取变量的数据流步骤)会保留流标签集,但其他步骤可能会添加或删除流标签。清理程序,特别是,只是删除一些或所有流标签的数据流步骤。值的初始流标签集由产生该值的源节点确定。类似地,接收节点可以指定传入值需要具有一定的流标签(或一组流标签)才能使流被标记为潜在的漏洞。

示例

作为使用流标签的示例,我们将展示如何编写一个查询,该查询会标记对来自用户控制输入的 JSON 值的属性访问,我们尚未检查该值是否为 null,因此属性访问可能会导致运行时异常。

例如,我们希望标记以下代码

function test(str) {
  var data = JSON.parse(str);
  if (data.length > 0) {  // problematic: `data` may be `null`
    ...
  }
}

另一方面,以下代码不应该被标记

function test(str) {
  var data = JSON.parse(str);
  if (data && data.length > 0) { // unproblematic: `data` is first checked for nullness
    ...
  }
}

我们将首先尝试编写一个查询来查找此类问题,而无需流标签,并将遇到的困难作为引入流标签的动机,这将使查询更容易实现。

首先,让我们编写一个查询,它只标记从 JSON.parse 到属性访问基部的任何流

import javascript

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

  override predicate isSource(DataFlow::Node nd) {
    exists(JsonParserCall jpc |
      nd = jpc.getOutput()
    )
  }

  override predicate isSink(DataFlow::Node nd) {
    exists(DataFlow::PropRef pr |
      nd = pr.getBase()
    )
  }
}

from JsonTrackingConfig cfg, DataFlow::Node source, DataFlow::Node sink
where cfg.hasFlow(source, sink)
select sink, "Property access on JSON value originating $@.", source, "here"

请注意,我们使用标准库中的 JsonParserCall 类来模拟各种 JSON 解析器,包括标准 JSON.parse API 以及许多流行的 npm 包。

当然,如上所示,该查询会标记上述好的示例和坏的示例,因为我们还没有引入任何清理程序。

有许多方法可以直接或间接地检查空值。由于这不是本教程的主要重点,因此我们只展示如何模拟一个特定情况:如果已知某个变量 v 为真值,则它不能为 null。这种类型的条件很容易使用 BarrierGuardNode(或其对应物 SanitizerGuardNode 用于污染跟踪配置)来表达。屏障保护节点是一个数据流节点 b,它阻止通过其他节点 nd 的流,前提是在 b 检查的某个条件已知为真,即计算结果为真值。

在我们的例子中,屏障保护节点是某个变量 v 的使用,条件是该使用本身:它阻止通过任何使用 v 的流,其中保护使用已知为真值。在上面的第二个示例中,&& 左侧的 data 使用是一个屏障保护,阻止通过 && 右侧的 data 使用的流。在这一点上,我们知道 data 已计算为真值,因此它不能再为 null 了。

实现这个额外的条件很容易。我们实现了 DataFlow::BarrierGuardNode 的子类

class TruthinessCheck extends DataFlow::BarrierGuardNode, DataFlow::ValueNode {
  SsaVariable v;

  TruthinessCheck() {
    astNode = v.getAUse()
  }

  override predicate blocks(boolean outcome, Expr e) {
    outcome = true and
    e = astNode
  }
}

然后我们使用它来覆盖我们配置类中的 isBarrierGuard 谓词

override predicate isBarrierGuard(DataFlow::BarrierGuardNode guard) {
  guard instanceof TruthinessCheck
}

有了这些更改,我们现在会标记有问题的案例,而不会标记上面的无问题案例。

但是,就目前而言,我们的分析存在许多误报:如果我们读取 JSON 对象的属性,我们的分析将不再跟踪它,因此对所得值的属性访问将不会检查空值保护

function test(str) {
  var root = JSON.parse(str);
  if (root) {
    var payload = root.data;   // unproblematic: `root` cannot be `null` here
    if (payload.length > 0) {  // problematic: `payload` may be `null` here
      ...
    }
  }
}

我们可以尝试通过在我们的配置类中覆盖 isAdditionalFlowStep 来弥补这种情况,以跟踪通过属性读取的值

override predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
  succ.(DataFlow::PropRead).getBase() = pred
}

但这实际上并不能让我们标记上面的问题,因为一旦我们检查了 root 的真值,所有进一步的使用都被视为已清理。特别是,在 root.data 中对 root 的引用被清理了,因此不会发生通过属性读取的任何流跟踪。

问题是,当然,我们的清理程序清理得太多了。它不应该完全停止流,它应该只记录 root 本身已知为非空的事实。另一方面,对 root 的任何属性读取,都可能是空值,需要单独检查。

我们可以通过引入两个不同的流标签来实现这一点,jsonmaybe-null。前者表示我们正在处理的值来自 JSON 对象,后者表示它可能是 null。对 JSON.parse 的任何调用的结果都具有这两个标签。从具有标签 json 的值读取属性也会具有这两个标签。检查真值会删除 maybe-null 标签。访问具有 maybe-null 标签的值的属性应该被标记。

为了实现这一点,我们首先定义 DataFlow::FlowLabel 类的两个新子类

class JsonLabel extends DataFlow::FlowLabel {
  JsonLabel() {
    this = "json"
  }
}

class MaybeNullLabel extends DataFlow::FlowLabel {
  MaybeNullLabel() {
    this = "maybe-null"
  }
}

然后我们扩展上面的 isSource 谓词以通过覆盖两个参数的版本而不是一个参数的版本来跟踪流标签

override predicate isSource(DataFlow::Node nd, DataFlow::FlowLabel lbl) {
  exists(JsonParserCall jpc |
    nd = jpc.getOutput() and
    (lbl instanceof JsonLabel or lbl instanceof MaybeNullLabel)
  )
}

类似地,我们使 isSink 成为流标签感知的,并要求属性读取的基部具有 maybe-null 标签

override predicate isSink(DataFlow::Node nd, DataFlow::FlowLabel lbl) {
  exists(DataFlow::PropRef pr |
    nd = pr.getBase() and
    lbl instanceof MaybeNullLabel
  )
}

我们对 isAdditionalFlowStep 的定义现在需要指定两个流标签,一个前驱标签 predlbl 和一个后继标签 succlbl。除了指定从前驱节点 pred 到后继节点 succ 的流之外,它还要求 pred 具有标签 predlbl,并将标签 succlbl 添加到 succ。在我们的例子中,我们使用它来将 json 标签和 maybe-null 标签都添加到从标记为 json 的值(无论它是否具有 maybe-null 标签)读取的任何属性中。

override predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ,
                              DataFlow::FlowLabel predlbl, DataFlow::FlowLabel succlbl) {
  succ.(DataFlow::PropRead).getBase() = pred and
  predlbl instanceof JsonLabel and
  (succlbl instanceof JsonLabel or succlbl instanceof MaybeNullLabel)
}

最后,我们将 TruthinessCheckBarrierGuardNode 转换为 LabeledBarrierGuardNode,指定它只从经过清理的值中删除 maybe-null 标签(而不是 json 标签)。

class TruthinessCheck extends DataFlow::LabeledBarrierGuardNode, DataFlow::ValueNode {
  ...

  override predicate blocks(boolean outcome, Expr e, DataFlow::FlowLabel lbl) {
    outcome = true and
    e = astNode and
    lbl instanceof MaybeNullLabel
  }
}

以下是最终查询,表示为 路径查询,以便我们可以在 UI 中逐步检查从源到接收器的路径。

/** @kind path-problem */

import javascript
import DataFlow::PathGraph

class JsonLabel extends DataFlow::FlowLabel {
  JsonLabel() {
    this = "json"
  }
}

class MaybeNullLabel extends DataFlow::FlowLabel {
  MaybeNullLabel() {
    this = "maybe-null"
  }
}

class TruthinessCheck extends DataFlow::LabeledBarrierGuardNode, DataFlow::ValueNode {
  SsaVariable v;

  TruthinessCheck() {
    astNode = v.getAUse()
  }

  override predicate blocks(boolean outcome, Expr e, DataFlow::FlowLabel lbl) {
    outcome = true and
    e = astNode and
    lbl instanceof MaybeNullLabel
  }
}

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

  override predicate isSource(DataFlow::Node nd, DataFlow::FlowLabel lbl) {
    exists(JsonParserCall jpc |
      nd = jpc.getOutput() and
      (lbl instanceof JsonLabel or lbl instanceof MaybeNullLabel)
    )
  }

  override predicate isSink(DataFlow::Node nd, DataFlow::FlowLabel lbl) {
    exists(DataFlow::PropRef pr |
      nd = pr.getBase() and
      lbl instanceof MaybeNullLabel
    )
  }

  override predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ,
                             DataFlow::FlowLabel predlbl, DataFlow::FlowLabel succlbl) {
    succ.(DataFlow::PropRead).getBase() = pred and
    predlbl instanceof JsonLabel and
    (succlbl instanceof JsonLabel or succlbl instanceof MaybeNullLabel)
  }

  override predicate isBarrierGuard(DataFlow::BarrierGuardNode guard) {
    guard instanceof TruthinessCheck
  }
}

from JsonTrackingConfig cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink, "Property access on JSON value originating $@.", source, "here"

我们在 https://github.com/finos/plexus-interop 存储库上运行了此查询。许多结果是误报,因为该查询目前没有对我们可以检查值是否为空的许多方法进行建模。特别是,在属性引用 x.p 之后,我们隐式地知道 x 不能再为空,因为否则引用将抛出异常。对这一点进行建模将使我们能够消除大多数误报,但这超出了本教程的范围。

API

纯数据流配置隐式地使用一个名为“data”的流标签,表示数据值来自源。您可以使用谓词 DataFlow::FlowLabel::data(),它返回此流标签,作为它的符号名称。

污点跟踪配置添加了第二个流标签“taint”(DataFlow::FlowLabel::taint()),它类似于“data”,但包括已通过非值保持步骤(例如字符串操作)的值。

三个成员谓词中的每一个 isSourceisSinkisAdditionalFlowStep/isAdditionalTaintStep 都有一个使用默认流标签的版本,以及一个允许通过附加参数指定自定义流标签的版本。

对于 isSource,有一个附加参数指定哪些流标签应与源自该源的值相关联。如果指定了多个流标签,则每个值都与它们中的 所有 标签相关联。

对于 isSink,附加参数指定流入该源的值可能与哪些流标签相关联。如果指定了多个流标签,那么与它们中的 至少一个 相关联的任何值都将被配置考虑。

对于 isAdditionalFlowStep,有两个附加参数 predlblsucclbl,它们允许流步骤充当流标签转换器。如果与 predlbl 相关联的值到达附加步骤的起始节点,它将被传播到结束节点并与 succlbl 相关联。当然,predlblsucclbl 可以相同,表示流步骤保留此标签。对于单个 predlbl 也可能有多个 succlbl 值,反之亦然。

请注意,如果您没有限制 succlbl,那么它将被允许在所有流标签上进行范围。这可能会导致以前在路径上被阻止的标签重新出现,这通常不是您想要的。

支持流标签版本的 isBarrier 称为 isLabeledBarrier:与阻止任何流经过给定节点的 isBarrier 不同,它只阻止与指定流标签之一相关联的值的流。

使用流标签的标准查询

我们的一些标准安全查询使用流标签。您可以查看它们的实现,以了解如何在实践中使用流标签。

特别是,上面关于基本数据流限制部分提到的两个示例都来自使用流标签的标准安全查询。污染原型的合并调用 查询使用两个流标签来区分完全被污染的对象和部分被污染的对象。路径表达式中使用的不受控制的数据 查询使用四个流标签来跟踪用户控制的字符串是否可能是绝对路径,以及它是否可能包含 .. 组件。

  • ©GitHub, Inc.
  • 条款
  • 隐私