在 Ruby 中分析数据流¶
您可以使用 CodeQL 追踪数据流在 Ruby 程序中的流动路径,直到数据被使用的地方。
关于本文¶
本文介绍了 CodeQL 库在 Ruby 中如何实现数据流分析,并包含示例来帮助您编写自己的数据流查询。以下部分介绍了如何使用这些库进行本地数据流、全局数据流和污点追踪。有关数据流建模的更一般介绍,请参阅“关于数据流分析”。
注意
从 CodeQL 2.13.0 开始,此处描述的用于数据流的新模块化 API 与 CodeQL 的先前库一起提供。有关库如何更改以及如何将任何现有查询迁移到模块化 API 的信息,请参阅 CodeQL 查询编写的新数据流 API。
本地数据流¶
本地数据流追踪数据流在一个单一方法或可调用体内的流动路径。本地数据流比全局数据流更容易、更快且更精确。在研究更复杂的追踪之前,您应该始终考虑本地追踪,因为它足以满足许多查询的需要。
使用本地数据流¶
您可以通过导入 DataFlow
模块来使用本地数据流库。该库使用 Node
类来表示任何数据可以流经的元素。节点被划分为表达式节点 (ExprNode
) 和参数节点 (ParameterNode
)。您可以使用 asParameter
成员谓词将数据流 ParameterNode
映射到其相应的 Parameter
AST 节点。类似地,您可以使用 asExpr
成员谓词将数据流 ExprNode
映射到其在控制流库中的相应 ExprCfgNode
。
class Node {
/** Gets the expression corresponding to this node, if any. */
CfgNodes::ExprCfgNode asExpr() { ... }
/** Gets the parameter corresponding to this node, if any. */
Parameter asParameter() { ... }
...
}
您可以使用谓词 exprNode
和 parameterNode
从表达式和参数映射到它们的数据流节点
/**
* Gets a node corresponding to expression `e`.
*/
ExprNode exprNode(CfgNodes::ExprCfgNode e) { ... }
/**
* Gets the node corresponding to the value of parameter `p` at function entry.
*/
ParameterNode parameterNode(Parameter p) { ... }
请注意,由于 asExpr
和 exprNode
在数据流和控制流节点之间进行映射,因此您需要在控制流节点上调用 getExpr
成员谓词以映射到相应的 AST 节点,例如,通过编写 node.asExpr().getExpr()
。控制流图考虑了控制可以流经代码的每种方式,因此,在 AST 中,单个表达式节点可能与多个数据流和控制流节点相关联。
谓词 localFlowStep(Node nodeFrom, Node nodeTo)
如果从节点 nodeFrom
到节点 nodeTo
存在一个直接的数据流边,则该谓词为真。您可以通过使用 +
和 *
运算符来递归地应用该谓词,或者您可以使用预定义的递归谓词 localFlow
。
例如,您可以在零个或多个本地步骤中查找从表达式 source
到表达式 sink
的数据流
DataFlow::localFlow(source, sink)
使用本地污点追踪¶
本地污点追踪扩展了本地数据流,以包括未保留值的流动步骤,例如,字符串操作。例如
temp = x
y = temp + ", " + temp
如果 x
是一个受污染的字符串,那么 y
也受污染。
本地污点追踪库位于 TaintTracking
模块中。与本地数据流一样,谓词 localTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo)
如果从节点 nodeFrom
到节点 nodeTo
存在一个直接的污点传播边,则该谓词为真。您可以通过使用 +
和 *
运算符来递归地应用该谓词,或者您可以使用预定义的递归谓词 localTaint
。
例如,您可以在零个或多个本地步骤中查找从表达式 source
到表达式 sink
的污点传播
TaintTracking::localTaint(source, sink)
使用本地源¶
在探索两个表达式之间的本地数据流或污点传播时,您通常会将表达式限制在与您的调查相关的范围内。下一节将给出一些具体的示例,但首先介绍本地源的概念很有帮助。
本地源是一个没有本地数据流流入它的数据流节点。因此,它是数据流的本地起源,即创建新值的地方。这包括参数(它们只接收来自全局数据流的值)和大多数表达式(因为它们不是值保留的)。LocalSourceNode
类表示也是本地源的数据流节点。它提供了一个有用的成员谓词 flowsTo(DataFlow::Node node)
,如果从本地源到 node
存在本地数据流,则该谓词为真。
本地数据流示例¶
此查询查找传递给每个对 File.open
的调用中的文件名参数
import codeql.ruby.DataFlow
import codeql.ruby.ApiGraphs
from DataFlow::CallNode call
where call = API::getTopLevelMember("File").getAMethodCall("open")
select call.getArgument(0)
请注意使用 API
模块来引用库方法。有关更多信息,请参阅“在 Ruby 中使用 API 图”。
不幸的是,这只会给出参数中的表达式,而不是可能传递给它的值。因此,我们使用本地数据流查找流入参数的所有表达式
import codeql.ruby.DataFlow
import codeql.ruby.ApiGraphs
from DataFlow::CallNode call, DataFlow::ExprNode expr
where
call = API::getTopLevelMember("File").getAMethodCall("open") and
DataFlow::localFlow(expr, call.getArgument(0))
select call, expr
许多表达式流向同一个调用。如果您运行此查询,您可能会注意到,当一个表达式流向一个调用时,您会得到多个该表达式的 DataFlow 节点(注意 call
列中重复的位置)。我们主要对这些节点中的“第一个”感兴趣,这可以称为文件名节点的本地源。为了将结果限制为文件名的本地源,并同时提高分析效率,我们可以使用 CodeQL 类 LocalSourceNode
。我们可以更新查询以指定 expr
是 LocalSourceNode
的一个实例。
import codeql.ruby.DataFlow
import codeql.ruby.ApiGraphs
from DataFlow::CallNode call, DataFlow::ExprNode expr
where
call = API::getTopLevelMember("File").getAMethodCall("open") and
DataFlow::localFlow(expr, call.getArgument(0)) and
expr instanceof DataFlow::LocalSourceNode
select call, expr
将结果限制为文件名的本地源的另一种方法是通过强制转换来实现。这将允许我们像这样在 LocalSourceNode
上使用成员谓词 flowsTo
import codeql.ruby.DataFlow
import codeql.ruby.ApiGraphs
from DataFlow::CallNode call, DataFlow::ExprNode expr
where
call = API::getTopLevelMember("File").getAMethodCall("open") and
expr.(DataFlow::LocalSourceNode).flowsTo(call.getArgument(0))
select call, expr
作为替代方案,我们可以更直接地询问 expr
是第一个参数的本地源,方法是通过谓词 getALocalSource
import codeql.ruby.DataFlow
import codeql.ruby.ApiGraphs
from DataFlow::CallNode call, DataFlow::ExprNode expr
where
call = API::getTopLevelMember("File").getAMethodCall("open") and
expr = call.getArgument(0).getALocalSource()
select call, expr
所有这三个查询都给出相同的结果。现在,每个调用只有一个表达式。
我们可能仍然有多个表达式流向一个调用的情况,但它们会流经不同的代码路径(可能是由于控制流分支)。
我们可能希望使源更具体,例如,一个方法或块的参数。此查询查找将参数用作打开文件时的名称的情况
import codeql.ruby.DataFlow
import codeql.ruby.ApiGraphs
from DataFlow::CallNode call, DataFlow::ParameterNode p
where
call = API::getTopLevelMember("File").getAMethodCall("open") and
DataFlow::localFlow(p, call.getArgument(0))
select call, p
使用通过参数提供的确切名称可能过于严格。如果我们想知道参数是否影响文件名,我们可以使用污点追踪而不是数据流。此查询查找对 File.open
的调用,其中文件名来自参数
import codeql.ruby.DataFlow
import codeql.ruby.TaintTracking
import codeql.ruby.ApiGraphs
from DataFlow::CallNode call, DataFlow::ParameterNode p
where
call = API::getTopLevelMember("File").getAMethodCall("open") and
TaintTracking::localTaint(p, call.getArgument(0))
select call, p
全局数据流¶
全局数据流追踪数据流在整个程序中的流动路径,因此比本地数据流更强大。但是,全局数据流比本地数据流精度更低,分析通常需要更多时间和内存才能完成。
注意
您可以通过创建路径查询来在 CodeQL 中建模数据流路径。要在适用于 VS Code 的 CodeQL 中查看由路径查询生成的数据流路径,您需要确保它具有正确的元数据和
select
子句。有关更多信息,请参阅 创建路径查询。
使用全局数据流¶
您可以通过实现签名 DataFlow::ConfigSig
并应用模块 DataFlow::Global<ConfigSig>
来使用全局数据流库
import codeql.ruby.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, "Dataflow to $@.", sink, sink.toString()
使用全局污点追踪¶
全局污点追踪与局部污点追踪的关系,类似于全局数据流与局部数据流的关系。也就是说,全局污点追踪在全局数据流的基础上,增加了额外的非值保持步骤。全局污点追踪库可以通过将模块 TaintTracking::Global<ConfigSig>
应用于配置,而不是 DataFlow::Global<ConfigSig>
来使用。
import codeql.ruby.DataFlow
import codeql.ruby.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
(定义在模块codeql.ruby.dataflow.RemoteFlowSources
中)表示来自远程网络输入的数据流。这对于在网络服务中查找安全问题非常有用。 - 库
Concepts
(定义在模块codeql.ruby.Concepts
中)包含DataFlow::Node
的几个子类,它们与安全相关,例如FileSystemAccess
和SqlExecution
。
对于全局流,将源限制为 LocalSourceNode
的实例也是有用的。预定义的源通常会这样做。
类层次结构¶
DataFlow::Node
- 一个充当数据流节点的元素。DataFlow::LocalSourceNode
- 作为数据流节点的本地数据源。DataFlow::ExprNode
- 一个充当数据流节点的表达式。DataFlow::ParameterNode
- 一个参数数据流节点,表示方法/代码块入口处参数的值。RemoteFlowSource
- 来自网络/远程输入的数据流。Concepts::SystemCommandExecution
- 一个执行操作系统命令(例如通过生成新进程)的数据流节点。Concepts::FileSystemAccess
- 一个执行文件系统访问(包括读取和写入数据、创建和删除文件和文件夹、检查和更新权限等)的数据流节点。Concepts::Path::PathNormalization
- 一个执行路径规范化的数据流节点。这通常需要为了安全地访问路径。Concepts::CodeExecution
- 一个动态执行 Ruby 代码的数据流节点。Concepts::SqlExecution
- 一个执行 SQL 语句的数据流节点。Concepts::HTTP::Server::RouteSetup
- 一个在服务器上设置路由的数据流节点。Concepts::HTTP::Server::HttpResponse
- 一个在服务器上创建 HTTP 响应的数据流节点。
全局数据流示例¶
- 以下全局污点追踪查询查找文件系统访问中可以由远程用户控制的路径参数。
- 由于这是一个污点追踪查询,因此使用了
TaintTracking::Global<ConfigSig>
模块。 - 谓词
isSource
将源定义为任何作为RemoteFlowSource
实例的数据流节点。 - 谓词
isSink
将汇点定义为任何文件系统访问中的路径参数,使用Concepts
库中的FileSystemAccess
。
- 由于这是一个污点追踪查询,因此使用了
import codeql.ruby.DataFlow
import codeql.ruby.TaintTracking
import codeql.ruby.Concepts
import codeql.ruby.dataflow.RemoteFlowSources
module RemoteToFileConfiguration implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
predicate isSink(DataFlow::Node sink) {
sink = any(FileSystemAccess fa).getAPathArgument()
}
}
module RemoteToFileFlow = TaintTracking::Global<RemoteToFileConfiguration>;
from DataFlow::Node input, DataFlow::Node fileAccess
where RemoteToFileFlow::flow(input, fileAccess)
select fileAccess, "This file access uses data from $@.", input, "user-controllable input."
- 以下全局数据流查询查找调用
File.open
的情况,其中文件名参数来自环境变量。 - 由于这是一个数据流查询,因此使用了
DataFlow::Global<ConfigSig>
模块。 - 谓词
isSource
将源定义为表示对ENV
哈希的查找的表达式节点。 - 谓词
isSink
将汇点定义为任何对File.open
的调用中的第一个参数。
- 由于这是一个数据流查询,因此使用了
import codeql.ruby.DataFlow
import codeql.ruby.controlflow.CfgNodes
import codeql.ruby.ApiGraphs
module EnvironmentToFileConfiguration implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
exists(ExprNodes::ConstantReadAccessCfgNode env |
env.getExpr().getName() = "ENV" and
env = source.asExpr().(ExprNodes::ElementReferenceCfgNode).getReceiver()
)
}
predicate isSink(DataFlow::Node sink) {
sink = API::getTopLevelMember("File").getAMethodCall("open").getArgument(0)
}
}
module EnvironmentToFileFlow = DataFlow::Global<EnvironmentToFileConfiguration>;
from DataFlow::Node environment, DataFlow::Node fileOpen
where EnvironmentToFileFlow::flow(environment, fileOpen)
select fileOpen, "This call to 'File.open' uses data from $@.", environment,
"an environment variable"
进一步阅读¶
- 使用路径查询探索数据流 在 GitHub 文档中。