为 Java 和 Kotlin 定制库模型¶
您可以对控制任何框架或库中数据流的方法和可调用项进行建模。这对于自定义框架或不受标准 CodeQL 库支持的小众库特别有用。
注意
针对 Kotlin 的 CodeQL 分析目前处于测试阶段。在测试期间,对 Kotlin 代码的分析及其随附文档不会像其他语言那样全面。
测试版通知 - 不稳定的 API
使用数据扩展进行库定制目前处于测试阶段,可能会发生变化。
在测试阶段,可能会对此格式进行重大更改。
关于本文¶
本文包含有关如何在数据扩展文件中为 Java 依赖项定义自定义模型(摘要、接收器和源)的参考材料。
创建自己的模型的最佳方法是使用适用于 Visual Studio Code 的 CodeQL 扩展中的 CodeQL 模型编辑器。模型编辑器会自动引导您完成定义模型的过程,显示您需要定义的属性和可用的选项。您可以将生成的模型作为数据扩展文件保存在 CodeQL 模型包中,并在不担心语法的情况下使用它们。
有关更多信息,请参阅 GitHub 文档中的使用 CodeQL 模型编辑器。
关于数据扩展¶
您可以通过在数据扩展文件中定义代码依赖项的模型(摘要、接收器和源)来自定义分析。每个模型都定义了库或框架的一个或多个元素的行为,例如方法和可调用项。运行数据流分析时,这些模型会扩展数据流分析跟踪的潜在源和接收器,并提高结果的精度。
大多数安全查询都会搜索从不受信任的输入源到代表漏洞的接收器的路径。这称为污点跟踪。每个源都是数据流分析跟踪受污染数据的起点,每个接收器都是终点。
污点跟踪查询还需要知道数据如何流经未包含在源代码中的元素。这些元素被建模为摘要。摘要模型使查询能够综合通过存储库中未存储的依赖项代码中的元素的流行为。
用于在扩展文件中定义元素的语法¶
每个元素模型都使用数据扩展定义,其中每个元组构成一个模型。用于扩展 CodeQL 附带的标准 Java 查询的数据扩展文件是一个 YAML 文件,格式如下:
extensions:
- addsTo:
pack: codeql/java-all
extensible: <name of extensible predicate>
data:
- <tuple1>
- <tuple2>
- ...
每个 YAML 文件可以包含一个或多个顶级扩展。
addsTo
定义 CodeQL 包名称和扩展注入到的可扩展谓词。data
定义作为值注入可扩展谓词的一行或多行元组。列数及其类型必须与可扩展谓词的定义相匹配。
数据扩展使用联合语义,这意味着将单个可扩展谓词的所有扩展的元组组合在一起,删除重复项,并且可以通过引用可扩展谓词来查询所有剩余的元组。
用于在 Java 和 Kotlin 中创建自定义模型的可扩展谓词¶
适用于 Java 和 Kotlin 分析的 CodeQL 库公开了以下可扩展谓词:
sourceModel(package, type, subtypes, name, signature, ext, output, kind, provenance)
。这用于对潜在受污染数据的来源进行建模。使用此谓词定义的源的kind
决定了它们与哪个威胁模型相关联。可以使用不同的威胁模型来自定义分析中使用的源。有关更多信息,请参阅“威胁模型”。sinkModel(package, type, subtypes, name, signature, ext, input, kind, provenance)
。这用于对可能以使代码易受攻击的方式使用受污染数据的接收器进行建模。summaryModel(package, type, subtypes, name, signature, ext, input, output, kind, provenance)
。这用于对流经元素的流量进行建模。neutralModel(package, type, name, signature, kind, provenance)
。这类似于摘要模型,但用于对对数据流分析影响较小的值的流进行建模。手动中性模型(其来源例如manual
或ai-manual
)会覆盖生成的摘要模型(其来源例如df-generated
),以便忽略该摘要。除此之外,中性模型对数据流调度逻辑的影响很小,超出了本文档的讨论范围。
使用数据扩展文件中定义的模型填充可扩展谓词。
自定义模型定义示例¶
本节中的示例摘自 GitHub 发布的标准 CodeQL Java 查询包。它们演示了如何添加元组以扩展标准查询使用的可扩展谓词。
示例:java.sql
包中的污点接收器¶
此示例显示 Java 查询包如何将 execute
方法的参数建模为 SQL 注入接收器。这是位于 java.sql
包中的 Statement
类中的 execute
方法。
public static void taintsink(Connection conn, String query) throws SQLException {
Statement stmt = conn.createStatement();
stmt.execute(query); // The argument to this method is a SQL injection sink.
}
我们需要通过更新数据扩展文件来向 sinkModel
(package, type, subtypes, name, signature, ext, input, kind, provenance) 可扩展谓词添加元组。
extensions:
- addsTo:
pack: codeql/java-all
extensible: sinkModel
data:
- ["java.sql", "Statement", True, "execute", "(String)", "", "Argument[0]", "sql-injection", "manual"]
由于我们要添加新的接收器,因此我们需要向 sinkModel
可扩展谓词添加元组。前五个值标识要建模为接收器的可调用项(在本例中为方法)。
- 第一个值
java.sql
是包名称。 - 第二个值
Statement
是包含该方法的类(类型)的名称。 - 第三个值
True
是一个标志,指示接收器是否也适用于该方法的所有重写。 - 第四个值
execute
是方法名称。 - 第五个值
(String)
是方法输入类型签名。
第六个值应留空,并且超出了本文档的讨论范围。其余值用于定义接收器的访问路径
、类型
和来源
。
- 第七个值
Argument[0]
是传递给该方法的第一个参数的访问路径
,这意味着这是接收器的位置。 - 第八个值
sql-injection
是接收器的类型。接收器类型用于定义接收器在其中起作用的查询。在这种情况下 - SQL 注入查询。 - 第九个值
manual
是接收器的来源,用于识别接收器的起源。
示例:来自 java.net
包的污点源¶
此示例展示了 Java 查询包如何将 getInputStream
方法的返回值建模为 remote
源。这是位于 java.net
包中的 Socket
类中的 getInputStream
方法。
public static void tainted(Socket socket) throws IOException {
InputStream stream = socket.getInputStream(); // The return value of this method is a remote source of taint.
...
}
我们需要通过更新数据扩展文件,向 sourceModel
(包、类型、子类型、名称、签名、扩展、输出、种类、来源) 可扩展谓词添加一个元组。
extensions:
- addsTo:
pack: codeql/java-all
extensible: sourceModel
data:
- ["java.net", "Socket", False, "getInputStream", "()", "", "ReturnValue", "remote", "manual"]
由于我们要添加一个新源,因此我们需要向 sourceModel
可扩展谓词添加一个元组。前五个值标识要建模为源的可调用对象(在本例中为方法)。
- 第一个值
java.net
是包名。 - 第二个值
Socket
是包含源的类的名称(类型)。 - 第三个值
False
是一个标志,指示源是否也适用于该方法的所有重写。 - 第四个值
getInputStream
是方法名称。 - 第五个值
()
是方法输入类型签名。
第六个值应留空,并且不在本文档的范围内。其余值用于定义源的 访问路径
、种类
和 来源
。
- 第七个值
ReturnValue
是方法返回的访问路径,这意味着返回值应被视为污点输入的来源。 - 第八个值
remote
是源的种类。源种类用于定义源在范围内的威胁模型。remote
适用于许多与安全相关的查询,因为它表示来自不受信任数据的远程源。例如,SQL 注入查询使用remote
源。有关更多信息,请参阅“威胁模型”。 - 第九个值
manual
是源的来源,用于识别源的起源。
示例:添加通过 concat
方法的流¶
此示例展示了 Java 查询包如何为简单情况建模通过方法的流。此模式涵盖了许多我们需要总结通过存储在存储库外部的库或框架中的方法的流的情况。
public static void taintflow(String s1, String s2) {
String t = s1.concat(s2); // There is taint flow from s1 and s2 to t.
...
}
我们需要通过更新数据扩展文件,向 summaryModel
(包、类型、子类型、名称、签名、扩展、输入、输出、种类、来源) 可扩展谓词添加元组
extensions:
- addsTo:
pack: codeql/java-all
extensible: summaryModel
data:
- ["java.lang", "String", False, "concat", "(String)", "", "Argument[this]", "ReturnValue", "taint", "manual"]
- ["java.lang", "String", False, "concat", "(String)", "", "Argument[0]", "ReturnValue", "taint", "manual"]
由于我们要添加通过方法的流,因此我们需要向 summaryModel
可扩展谓词添加元组。每个元组定义从一个参数到返回值的流。第一行定义从限定符(示例中的 s1
)到返回值(示例中的 t
)的流,第二行定义从第一个参数(示例中的 s2
)到返回值(示例中的 t
)的流。
前五个值标识要建模为摘要的可调用对象(在本例中为方法)。以上两行的值相同,因为我们为同一个方法添加了两个摘要。
- 第一个值
java.lang
是包名。 - 第二个值
String
是类(类型)名称。 - 第三个值
False
是一个标志,指示摘要是否也适用于该方法的所有重写。 - 第四个值
concat
是方法名称。 - 第五个值
(String)
是方法输入类型签名。
第六个值应留空,并且不在本文档的范围内。其余值用于定义摘要的 访问路径
、种类
和 来源
。
- 第七个值是输入的访问路径(数据从何处流入)。
Argument[this]
是限定符的访问路径(示例中的s1
),Argument[0]
是第一个参数的访问路径(示例中的s2
)。 - 第八个值
ReturnValue
是输出的访问路径(数据流向何处),在本例中为ReturnValue
,这意味着输入流向返回值。 - 第九个值
taint
是流的种类。taint
表示污点通过调用传播。 - 第十个值
manual
是摘要的来源,用于识别摘要的起源。
示例:添加通过 map
方法的流¶
此示例展示了 Java 查询包如何建模通过方法的更复杂流。在这里,我们对高阶方法和集合类型中的流进行建模。
public static void taintflow(Stream<String> s) {
Stream<String> l = s.map(e -> e.concat("\n"));
...
}
我们需要通过更新数据扩展文件,向 summaryModel
(包、类型、子类型、名称、签名、扩展、输入、输出、种类、来源) 可扩展谓词添加元组
extensions:
- addsTo:
pack: codeql/java-all
extensible: summaryModel
data:
- ["java.util.stream", "Stream", True, "map", "(Function)", "", "Argument[this].Element", "Argument[0].Parameter[0]", "value", "manual"]
- ["java.util.stream", "Stream", True, "map", "(Function)", "", "Argument[0].ReturnValue", "ReturnValue.Element", "value", "manual"]
由于我们要添加通过方法的流,因此我们需要向 summaryModel
可扩展谓词添加元组。每个元组定义构成通过 map
方法的总流的一部分流。前五个值标识要建模为摘要的可调用对象(在本例中为方法)。以上两行的值相同,因为我们为同一个方法添加了两个摘要。
- 第一个值
java.util.stream
是包名。 - 第二个值
Stream
是类(类型)名称。 - 第三个值
True
是一个标志,指示摘要是否也适用于该方法的所有重写。 - 第四个值
map
是方法名称。 - 第五个值
Function
是方法输入类型签名。
第六个值应留空,并且不在本文档的范围内。其余值用于定义摘要定义的 访问路径
、种类
和 来源
。
- 第七个值是
输入
的访问路径(数据从何处流入)。 - 第八个值是
输出
的访问路径(数据流向何处)。
对于第一行
- 第七个值是
Argument[this].Element
,这是限定符元素的访问路径(示例中流s
的元素)。 - 第八个值是
Argument[0].Parameter[0]
,这是map
的Function
参数的第一个参数的访问路径(示例中的 lambda 参数e
)。
对于第二行
- 第七个值是
Argument[0].ReturnValue
,这是map
的Function
参数的返回值的访问路径(示例中 lambda 的返回值)。 - 第八个值是
ReturnValue.Element
,这是map
返回值的元素的访问路径(示例中流l
的元素)。
对于两行的其余值
- 第九个值
value
是流的种类。value
表示值被保留。 - 第十个值
manual
是摘要的来源,用于识别摘要的起源。
也就是说,第一行指定值可以从限定符流的元素流入提供给 map
的函数的第一个参数。第二行指定值可以从函数的返回值流入从 map
返回的流的元素。
示例:添加 neutral
方法¶
此示例展示了 Java 查询包如何将 now
方法建模为相对于流而言是中性的。中性模型用于定义没有流经方法。
public static void taintflow() {
Instant t = Instant.now(); // There is no flow from now to t.
...
}
我们需要通过更新数据扩展文件,向 neutralModel
(包、类型、名称、签名、种类、来源) 可扩展谓词添加一个元组。
extensions:
- addsTo:
pack: codeql/java-all
extensible: neutralModel
data:
- ["java.time", "Instant", "now", "()", "summary", "manual"]
由于我们要添加一个中性模型,因此我们需要向 neutralModel
可扩展谓词添加元组。前四个值标识要建模为中性的可调用对象(在本例中为方法),第五个值是种类,第六个值是中性的来源。
- 第一个值
java.time
是包名。 - 第二个值
Instant
是类(类型)名称。 - 第三个值
now
是方法名称。 - 第四个值
()
是方法输入类型签名。 - 第五个值
summary
是中性点的类型。 - 第六个值
manual
是中性点的来源。
威胁模型¶
注意
威胁模型目前处于测试阶段,可能会发生变化。在测试阶段,只有 Java 和 C# 分析支持威胁模型。
威胁模型是一类命名的可独立启用或禁用的数据流源。威胁模型允许您控制要视为不安全的数据流源集。例如,一个代码库可能只将远程 HTTP 请求视为受污染的,而另一个代码库也可能将来自本地文件的数据视为不安全的。您可以使用威胁模型来确保在 CodeQL 分析中使用相关污染源。
sourceModel
的 kind
属性决定了源与哪个威胁模型相关联。主要有两类:
remote
表示来自网络的请求和响应。local
表示来自本地文件的数据(file
)、命令行参数(commandargs
)、数据库读取(database
)和环境变量(environment
)。
运行 CodeQL 分析时,默认包含 remote
威胁模型。在使用 CodeQL CLI 和 GitHub 代码扫描时,您可以选择性地包含其他威胁模型。有关更多信息,请参阅使用 CodeQL 查询分析代码和自定义代码扫描的高级设置。