但我们查询数据流的时候,可能数据流会经过很多个节点,但是普通的查询结果一般只展示入口节点和出口节点,那中间节点是什么呢,万一跨越多个文件,不可能自己慢慢去看吧。
这个时候就需要使用 PathGraph 让 CodeQL 帮我们将中间的节点也都展示出来,方便我们进行查看。
更新 CodeQL
首先要做的是升级你的 CodeQL (如果你用的还是老版本的话 :),我直接升级到了最新版 CodeQL CLI v2.23.5
-
我是 MAC 所以我这里选择
codeql-osx64.zip,这里按照自己的系统类型选择即可 -
然后将下载下来的 zip 解压缩,覆盖本来的 CodeQL 文件夹即可。
-
这里有个小插曲,当初接触 CodeQL 的时候,分不清
CodeQL CLI和CodeQL SDK,然后我安装的时候,文件夹是下面这样写的,我这里文件夹名字其实是错的,我用红色字体写出来的应该才是正确的文件夹。 -
好的,让我把这个历史遗留问题改了吧,改好后的结果如下:
-
然后第三步是将解压缩的 zip 文件里的内容,给覆盖到
CodeQL-CLI里面即可。
可视化查询数据流
这个时候就需要我们显示指定元数据中的 @kind 了。这里需要注意要在元数据中写明 @kind path-problem,然后还要导入 import DataFlow::PathGraph ,最后 select 也要满足 4 个参数的条件。
select p1,p2,p3,p4
p1 是指数据流节点从 source 开始展示还是 sink 开始展示
p2,一般是 source
p3, 一般是 sink
p4, 一般是文字提示信息
/**
* @kind path-problem
* @problem.severity error
* @id wegoattest/remote-url-creation
*/
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph
class Configuration extends DataFlow::Configuration {
Configuration() {
this = "Configuration"
}
override predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource
}
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::PathNode source, DataFlow::PathNode sink, Configuration config
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "execute sink called with untrusted data"
然后就可以执行代码获取响应。
这个时候,CodeQL 插件就会在右侧展示整个数据流的 Path,将所有的节点都展示出来了。
新版数据流可视化:使用模块替换类
当我运行上述代码的时候,我发现 VS Code 提示我,这个 DataFlow::PathGraph 以后可能会弃用了,那么新的查询数据流节点的代码是什么呢?结果花了好多时间才找到,真的是太难了。
/**
* @kind path-problem
* @problem.severity error
* @id wegoattest/remote-url-creation
*/
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
module MyFlowConfiguration implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource
}
predicate isSink(DataFlow::Node sink) {
exists(Call call |
sink.asExpr() = call.getArgument(0) and
call.getCallee().(Constructor).getDeclaringType().hasQualifiedName("java.net", "URL")
)
}
}
module MyFlow = TaintTracking::Global<MyFlowConfiguration>;
import MyFlow::PathGraph
from MyFlow::PathNode source, MyFlow::PathNode sink
where MyFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "execute sink called with untrusted data"
首先我们自己定义一个模块 MyFlowConfiguration 继承 DataFlow::ConfigSig ,然后定义模块 MyFlow = TaintTracking::Global<MyFlowConfiguration>; 来全局进行污点追踪。
其中还有一个比较重要的是这么一句 import MyFlow::PathGraph 这样才能让 CodeQL 给你展示数据流节点
这样我们就用新的方法查询到了 数据流的节点,并且没有提示我们某个方法在未来要弃用了,很完美,但是这个过程也很痛苦,毕竟英文资料是真的难查。
而且前一章里的净化函数和续接函数也发生了一些变化。
我们将上一章讲净化函数 isSanitizer 的代码改成新版的 isBarrier 净化函数的代码
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
module MyFlowConfiguration implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
exists(source.asParameter())
}
predicate isSink(DataFlow::Node sink) {
exists(Method method, MethodCall call |
// 假设 createSecretFileWithRandomContents() 就是危险函数
method.hasName("createSecretFileWithRandomContents")
and
call.getMethod() = method and
sink.asExpr() = call.getArgument(0)
)
}
// predicate isBarrier(DataFlow::Node node) {
// exists(Method method, MethodCall call |
// // 假设经过 reset 函数后就不危险了
// method.hasName("reset") and call.getMethod() = method and node.asExpr() = call.getAnArgument())
// }
}
module MyFlow = TaintTracking::Global<MyFlowConfiguration>;
import MyFlow::PathGraph
from MyFlow::PathNode source, MyFlow::PathNode sink
where MyFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "This $@ is written to a log file.", source.getNode(),
"potentially sensitive information"
先把净化函数注释运行一遍,然后讲注释取消再运行一遍,然后查看一下区别吧。
净化函数被注释掉了
净化函数放开注释
isSanitizer 、isSanitizerIn 、isBarrier、isBarrierIn 举例说明
一般情况下,isSanitizer、isSanitizerIn 使用场景是 明确的消毒/编码函数
isBarrier、isBarrierIn 使用场景是 加密、序列化等数据转换
// 假设现在有一段 java 代码如下:
// userInput → validateInput() → databaseQuery()
// 我们假设 isSource 就是 userInput
// 假设 isSink 就是 databseQuery()
override predicate isSanitizer、isSanitizerIn(DataFlow::Node node) {
exists(CallExpr call | call.getCalleeName() = "validateInput"
and node.asExpr() = call )
}
override predicate isBarrier、isBarrierIn(DataFlow::Node node) {
exists(CallExpr call | call.getCalleeName() = "encryptData"
and node.asExpr() = call )
}
下面是 AI 生成的,不一定是对的 :)
数据流续接函数也是一样 isAdditionalTaintStep 换成了 isAdditionalFlowStep,就不具体展示了。