CodeQL 数据流库的隐式读取(Implicit Reads)支持:允许非空访问路径流入 Sink 与自定义污点步骤

原创2026-10-07 23:57:511,825 阅读
文章标签:静态分析SAST应用安全漏洞扫描代码质量

CodeQL 数据流库的隐式读取(Implicit Reads)支持:允许非空访问路径流入 Sink 与自定义污点步骤

导读

本文基于 CodeQL 开源仓库中 C# 数据流库的变更说明(csharp/old-change-notes/2021-06-24-dataflow-implicit-reads.md),深入讲解 CodeQL 数据流(DataFlow)与污点追踪(TaintTracking)库中"配置级(Configuration-specific)就地读取步骤"(in-place read steps)这一能力:它允许查询作者在 sink 与自定义污点步骤处,指定能够接受"非空访问路径"(non-empty access paths)的流动。读完本文,你将掌握 allowImplicitRead 与 defaultImplicitTaintRead 的语义、实现位置与自定义查询的接入方式,从而写出更精准、更少误报的 CodeQL 安全查询。

背景:访问路径(Access Path)与读取步骤

在 CodeQL 数据流分析中,值会携带一个访问路径(access path),描述其当前所指向的字段/元素位置,例如 obj.Field 或 arr[i]。当数据从源(source)流向汇点(sink)时,分析引擎需要模拟对对象成员的"读"(read)与"写"(store)操作来更新访问路径。

在旧版本的数据流库中,读取步骤只在标准数据流语义内进行:一个携带非空访问路径的值若想流入某个 sink 或自定义污点步骤,通常要求该处存在一次显式的"取元素/取字段"读取(read step)把路径"读空"。这导致许多实际场景被漏报——例如 sink 直接消费集合中的某个元素,而数据以 collection.Element 这样的非空路径到达时,若没有显式读取步骤,流动会被切断。

2021-06-24 的这项变更正是为了解决该问题:数据流库被扩展,支持在 sink 与自定义污点步骤处进行"配置级就地读取",即:

现在可以指定:sink 能够接受携带非空访问路径的流入。

核心变更:配置级就地读取步骤

原变更说明的核心内容如下(lgtm、codescanning 两个平台均生效):

The DataFlow libraries have been augmented with support for Configuration-specific in-place read steps at, for example, sinks and custom taint steps. This means that it is now possible to specify sinks that accept flow with non-empty access paths.

将其拆解为三个要点:

  1. Configuration-specific(配置级):读取行为由每个数据流/污点配置(Configuration)自己声明,而不是全局统一强制,因此不同查询可以按需开启,避免全局开启带来的性能与精度代价。
  2. in-place read steps(就地读取步骤):在流动的终点(sink)或自定义污点步骤的输入处,就地执行一次隐式读取,无需在图上额外添加显式节点,即可消化掉访问路径。
  3. non-empty access paths(非空访问路径):此前 sink 一般只接受"空路径"流入;现在可以指定 sink 接受 x.field、x[i] 这类仍携带内容路径的值。

接口设计:allowImplicitRead

该能力通过数据流配置签名中的新谓词暴露给查询作者。在 shared/dataflow/codeql/dataflow/DataFlow.qll 中:

/**
 * Holds if an arbitrary number of implicit read steps of content `c` may be
 * taken at `node`.
 */
default predicate allowImplicitRead(Node node, ContentSet c) { none() }

要点解读:

  • 它是 DataFlow::Configuration 的默认谓词(default predicate),默认行为是 none(),即默认不开启任何隐式读取——保证旧查询行为不变,仅在查询作者显式覆盖时才启用。
  • 参数 node:允许发生隐式读取的节点(通常是 sink 或自定义污点步骤的输入节点)。
  • 参数 c:允许被隐式读取的内容(ContentSet,例如字段、索引元素等)。
  • 语义为"任意数量(arbitrary number)的隐式读取步骤",即允许连续多次读取,逐步消化嵌套路径。

污点追踪的默认开启策略:defaultImplicitTaintRead

在污点追踪库中,隐式读取被进一步内置为默认策略。见 shared/dataflow/codeql/dataflow/TaintTracking.qll 的 InputSig 签名:

/**
 * Holds if taint flow configurations should allow implicit reads of `c` at sinks
 * and inputs to additional taint steps defined in the flow `Config`.
 *
 * Note that this (deliberately) does not include at additional taint steps defined
 * globally in `defaultAdditionalTaintStep`. These models are expected to be precise
 * and therefore to not require implicit reads.
 */
bindingset[node]
predicate defaultImplicitTaintRead(Lang::Node node, Lang::ContentSet c);

随后在 AddTaintDefaults 模块中(TaintTracking.qll),默认的 allowImplicitRead 被组合为:

predicate allowImplicitRead(DataFlowLang::Node node, DataFlowLang::ContentSet c) {
  Config::allowImplicitRead(node, c)
  or
  (
    Config::isSink(node) or
    Config::isSink(node, _) or
    Config::isAdditionalFlowStep(node, _, _) or
    Config::isAdditionalFlowStep(node, _, _, _, _)
  ) and
  defaultImplicitTaintRead(node, c)
}

这一实现的工程含义:

  • 用户自定义优先:若查询在配置里显式覆盖了 allowImplicitRead,以显式声明为准。
  • 默认覆盖两类位置:当节点是 sink(无论带不带 flow state 的重载)或自定义污点步骤(additional flow step)的输入时,自动采用语言级 defaultImplicitTaintRead。
  • 刻意排除全局默认污点步骤:注释明确指出,全局 defaultAdditionalTaintStep 中的步骤不享受默认隐式读取,因为这些模型被期望足够精确、不需要隐式读取来补漏。

语言侧实现:C# 的默认内容集

C# 语言库为上述签名提供了具体实现。见 csharp/ql/lib/semmle/code/csharp/dataflow/internal/TaintTrackingPrivate.qll:

/**
 * Holds if default `TaintTracking::Configuration`s should allow implicit reads
 * of `c` at sinks and inputs to additional taint steps.
 */
bindingset[node]
predicate defaultImplicitTaintRead(DataFlow::Node node, DataFlow::ContentSet c) {
  exists(node) and
  (
    c.isElement()
    or
    c.isProperty(keyValuePairValue())
  )
}

从中可以读出 C# 默认允许被隐式读取的内容类型:

  • c.isElement():索引器 / 集合元素内容(如 list[i]、dict[key]),这是集合类 sink 最常见的路径形态。
  • c.isProperty(keyValuePairValue()):特指 System.Collections.Generic.KeyValuePair<TKey, TValue>.Value 这一属性——例如 Dictionary 迭代出的 KeyValuePair,其 Value 携带的内容路径可在 sink 处被就地读取。
  • exists(node) 表示不限定具体节点(任何 sink / 步骤输入均可),配合 bindingset[node] 做绑定集优化。

引擎实现:readSetEx 与隐式读节点

在引擎内部(shared/dataflow/codeql/dataflow/internal/DataFlowImplStage1.qll),隐式读取被建模为 readSetEx 的一个分支:

pragma[nomagic]
private predicate readSetEx(NodeEx node1, ContentSet c, NodeEx node2) {
  readEx(node1, c, node2) and
  stepFilter(node1, node2)
  or
  exists(Node n |
    node2.isImplicitReadNode(n) and
    Config::allowImplicitRead(n, c)
  |
    node1.asNode() = n and
    not fullBarrier(node1)
    or
    node1.isImplicitReadNode(n)
  )
}

理解要点:

  • node2.isImplicitReadNode(n) 判断目标节点是否为"隐式读节点"(TNodeImplicitRead,见 DataFlowImplCommon.qll 中的 asNodeOrImplicitRead / isImplicitReadNode)。
  • 只有当 Config::allowImplicitRead(n, c) 成立时,该隐式读才被启用——即上文所述"配置级"控制真正生效的地方。
  • 隐式读节点可串联(node1.isImplicitReadNode(n) 允许前一个节点本身也是隐式读节点),对应"任意数量的隐式读取步骤"语义。

在路径图(PathGraph)层面,DataFlowImpl.qll 提供了 getAnImplicitReadSuccessorAtSink,把"最后一次隐式读取(访问路径变空)"与"仍保留非空访问路径的隐式读取"区分开来,保证路径解释(path explanations)依然清晰可读。

自定义查询实战

要在自己的数据流/污点查询中启用该能力,有两种方式:

方式一:在污点配置中覆盖 allowImplicitRead

import csharp
import semmle.code.csharp.dataflow.TaintTracking

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

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

  // 允许在 sink 处就地读取集合元素内容
  override predicate allowImplicitRead(DataFlow::Node node, DataFlow::ContentSet c) {
    this.isSink(node) and c.isElement()
  }
}

方式二:直接依赖语言默认策略(推荐)

如果你使用的是 TaintTracking::Configuration(而非纯 DataFlow::Configuration),则无需任何覆盖:只要把 isSink 指向能消费集合元素 / KeyValuePair.Value 的表达式,AddTaintDefaults 就会自动为这些位置启用默认隐式读取,sink 即可接受携带非空访问路径的流入。

适用场景与注意事项

  • 典型场景:sink 为 foreach 循环体、LINQ 回调、Dictionary 迭代、集合参数传递等——污点到达时访问路径尚未被"读空"。
  • 精度控制:隐式读取是"放宽容忍"手段,会引入更多路径;因此默认只在 sink 与配置内自定义污点步骤处开启,且内容限定为元素与 KeyValuePair.Value,避免全局放行导致误报膨胀。
  • 性能:bindingset<a href="https://link.gitcode.com/i/300b6d101feb8bf3b56fb4f7f212eb47" target="_blank">node]、pragma[nomagic] 等标注(见 [TaintTrackingPrivate.qll、DataFlowImplStage1.qll)说明引擎对隐式读取分支做了绑定集与基数优化,实际启用时需结合查询规模评估。
  • 版本适用前提:本文描述的能力对应仓库中"2021-06-24"之后的数据流库实现,若你使用旧版 CodeQL 发行包,需确认 allowImplicitRead 谓词是否存在。

小结

2021-06-24-dataflow-implicit-reads 这项变更,为 CodeQL 数据流库引入了配置级就地隐式读取能力:查询作者可通过 allowImplicitRead 指定哪些 sink 与自定义污点步骤接受携带非空访问路径的流入;C# 污点库更进一步,通过 defaultImplicitTaintRead 对集合元素与 KeyValuePair.Value 内容默认放行。其实现横跨数据流引擎(DataFlowImplStage1.qll 的 readSetEx、DataFlowImpl.qll 的隐式读后继)与语言适配层(TaintTrackingPrivate.qll),是编写高召回、低误报 C# 安全查询时值得掌握的关键机制。

登录后查看全文
codeql