CodeQL数据流库通过对程序或功能的数据流图进行建模来实现对程序或功能的数据流分析。与抽象语法树不同,数据流图不反映程序的语法结构,而是在运行时对数据流过程序的方式进行建模。抽象语法树中的节点代表语法元素,例如语句或表达式。另一方面,数据流图中的节点表示在运行时带有值的语义元素。
一些AST节点(例如表达式)具有相应的数据流节点,而其他AST节点(例如if
语句)则没有。这是因为表达式在运行时被评估为一个值,而 if
语句纯粹是一个控制流构造,并且不携带值。还有一些数据流节点根本不与AST节点相对应。
数据流图中的边缘表示程序元素之间数据流的方式。例如,在表达式中,存在与子表达式和相对应的数据流节点,以及与整个表达式相对应的数据流节点。从对应于的节点到对应于的节点之间存在一条边,表示数据可能从到流动(因为表达式可以求值为)。同样,从对应的节点到对应于的节点都有一条边。x || y
x
y
x || y
x
x || y
x
x || y
x || y
x
y
x || y
本地数据流和全局数据流在考虑的边缘方面有所不同:本地数据流仅考虑属于同一功能的数据流节点之间的边缘,而忽略功能之间以及通过对象属性的数据流。但是,全局数据流也要考虑后者。污染跟踪将其他边沿引入数据流图中,这些边并不精确地对应于值流,而是模拟运行时某个值是否可以从另一个值中导出,例如通过字符串操作。
使用类计算数据流图,以对表示图节点的程序元素进行建模。节点之间的数据流使用谓词建模以计算图的边缘。
计算准确而完整的数据流图提出了若干挑战:
为了克服这些潜在的问题,库中对两种数据流进行了建模:
许多CodeQL查询都包含本地和全局数据流分析的示例。有关详细信息,请参见内置查询。
在标准库中,我们对“正常”数据流和污点跟踪进行了区分。常规数据流库用于分析每个步骤中数据值都被保留的信息流。
例如,如果您要跟踪不安全的对象x
(可能是一些不受信任的或潜在的恶意数据),则程序中的某个步骤可能会“更改”其值。因此,在诸如y=x+1
这样的简单过程中,正常的数据流分析将突出显示x,但不突出显示y。但是,由于y是从x派生的,因此受不受信任或“污染”的信息的影响,因此也会被污染。分析污点从x到y的流动称为污点跟踪。
在QL中,污点跟踪通过包括以下步骤来扩展数据流分析,在这些步骤中不必保留数据值,但仍会传播潜在的不安全对象。这些流程步骤在污点跟踪库中使用谓词进行建模,这些谓词用于确定污点是否在节点之间传播。
我们可以使用CodeQL跟踪Java程序数据流。
本地数据流是指单个方法或者可调用方法内的数据流,通常比全局数据流有着更容易、更快和更准确的优点,对于许多查询来说是足够的了。
本地数据流库位于DataFlow模块中
,该模块定义了Node类
表示数据可以流经的任何元素。Node
分为表达式节点(ExprNode
)和参数节点(ParameterNode
)。我们可以使用成员谓词asExpr
和asParameter
在数据流节点和表达式/参数之间进行映射:
class Node { /** Gets the expression corresponding to this node, if any. */ Expr asExpr() { ... } /** Gets the parameter corresponding to this node, if any. */ Parameter asParameter() { ... } ... }
或使用谓词exprNode
和parameterNode
:
/** * 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) { ... }
如果从节点nodeFrom
到节点nodeTo之间存在直连的数据流边线,则谓词localFlowStep(Node nodeFrom, Node nodeTo)
成立。我们可以使用+
和
*
运算符,也可以使用预定义的递归谓词localFlow(等效于localFlowStep*
)来递归地应用谓词。
例如,我们可以在0个或多个本地数据流步骤中找到从参数source
到表达式sink的流
:
DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink))
本地污点跟踪通过包含非保留值的流程步骤来扩展本地数据流。例如:
String temp = x;
String y = temp + ", " + temp;
如果x
是受污染的字符串,则y
也受污染。
本地污染跟踪库位于TaintTracking
模块中。像本地数据流一样,如果从nodeFrom
节点到nodeTo节点之间存在直接的污点传播边线,则谓词localTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo)
成立。您可以使用+和
*
运算符,也可以使用预定义的递归谓词localTaint
(等效于localTaintStep*
)来递归地应用谓词。
例如,我们可以在0个或多个本地数据流步骤中查找从参数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
然后,我们可以使source 更加具体,例如对公共参数的访问。以下查询查找将公共参数传递到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
的所有硬编码字符串。(回答)
全局数据流跟踪整个程序中的数据流,因此比本地数据流更强大。但是,全局数据流不如本地数据流精确,并且分析通常需要大量时间和内存才能执行。
您可以通过扩展类DataFlow::Configuration
来使用全局数据流库:
import semmle.code.java.dataflow.DataFlow class MyDataFlowConfiguration extends DataFlow::Configuration { MyDataFlowConfiguration() { this = "MyDataFlowConfiguration" } override predicate isSource(DataFlow::Node source) { ... } override predicate isSink(DataFlow::Node sink) { ... } }
这些谓词在配置中定义:
isSource
-定义数据可能来源isSink
-定义数据可能流向的位置isBarrier
—可选,限制数据流isAdditionalFlowStep
—可选,添加额外的数据流步骤特征谓词MyDataFlowConfiguration()
定义了配置的名称,因此"MyDataFlowConfiguration"
应该是个唯一的名称,例如类的名称。
使用谓词hasFlow(DataFlow::Node source, DataFlow::Node sink)
执行数据流分析:
from MyDataFlowConfiguration dataflow, DataFlow::Node source, DataFlow::Node sink
where dataflow.hasFlow(source, sink)
select source, "Data flow to $@.", sink, sink.toString()
全局污点跟踪是针对全局数据流而言,就像本地污点跟踪是针对本地数据流一样。也就是说,全局污点跟踪通过额外的non-value-preserving步骤扩展了全局数据流。我们可以通过扩展类TaintTracking::Configuration
来使用全局污点跟踪库:
import semmle.code.java.dataflow.TaintTracking class MyTaintTrackingConfiguration extends TaintTracking::Configuration { MyTaintTrackingConfiguration() { this = "MyTaintTrackingConfiguration" } override predicate isSource(DataFlow::Node source) { ... } override predicate isSink(DataFlow::Node sink) { ... } }
这些谓词在配置中定义:
isSource
-定义污点的可能来源isSink
-定义污点可能流向的位置 isSanitizer
—可选,限制污点流isAdditionalTaintStep
—可选,添加额外污点步骤与全局数据流相似,特征谓词MyTaintTrackingConfiguration()
定义配置的唯一名称。
污点跟踪分析是使用谓词hasFlow(DataFlow::Node source, DataFlow::Node sink)
执行的。
数据流库包含一些预定义的流sources。RemoteFlowSource
类(在semmle.code.java.dataflow.FlowSources中定义
)表示可由远程用户控制的数据流source
,这对于发现安全性问题很有用。
下述查询展示了使用远程用户输入作为数据源的污点跟踪配置。
import java import semmle.code.java.dataflow.FlowSources class MyTaintTrackingConfiguration extends TaintTracking::Configuration { MyTaintTrackingConfiguration() { this = "..." } override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } ... }
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
import semmle.code.java.dataflow.DataFlow class Configuration extends DataFlow::Configuration { Configuration() { this = "LiteralToURL Configuration" } override predicate isSource(DataFlow::Node source) { source.asExpr() instanceof StringLiteral } override predicate isSink(DataFlow::Node sink) { exists(Call call | sink.asExpr() = call.getArgument(0) and call.getCallee().(Constructor).getDeclaringType().hasQualifiedName("java.net", "URL") ) } } from DataFlow::Node src, DataFlow::Node sink, Configuration config where config.hasFlow(src, sink) select src, "This string constructs a URL $@.", sink, "here"
import java class GetenvSource extends MethodAccess { GetenvSource() { exists(Method m | m = this.getMethod() | m.hasName("getenv") and m.getDeclaringType() instanceof TypeSystem ) } }
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 ) } } class GetenvToURLConfiguration extends DataFlow::Configuration { GetenvToURLConfiguration() { this = "GetenvToURLConfiguration" } override predicate isSource(DataFlow::Node source) { source instanceof GetenvSource } override predicate isSink(DataFlow::Node sink) { exists(Call call | sink.asExpr() = call.getArgument(0) and call.getCallee().(Constructor).getDeclaringType().hasQualifiedName("java.net", "URL") ) } } from DataFlow::Node src, DataFlow::Node sink, GetenvToURLConfiguration config where config.hasFlow(src, sink) select src, "This environment variable constructs a URL $@.", sink, "here"