CodeQL 数据流库的隐式读取(Implicit Reads)支持:允许非空访问路径流入 Sink 与自定义污点步骤
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.
将其拆解为三个要点:
Configuration-specific(配置级):读取行为由每个数据流/污点配置(Configuration)自己声明,而不是全局统一强制,因此不同查询可以按需开启,避免全局开启带来的性能与精度代价。- in-place read steps(就地读取步骤):在流动的终点(sink)或自定义污点步骤的输入处,就地执行一次隐式读取,无需在图上额外添加显式节点,即可消化掉访问路径。
- 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# 安全查询时值得掌握的关键机制。