CodeQL 文档

为 JavaScript 自定义库模型

Beta 公告 - 不稳定 API

使用数据扩展进行的库自定义目前处于 Beta 阶段,可能会发生变化。

此格式在 Beta 阶段可能会出现重大更改。

可以通过在数据扩展文件中添加库模型来自定义 JavaScript 分析。

JavaScript 的数据扩展是一个 YAML 文件,其格式如下

extensions:
  - addsTo:
      pack: codeql/javascript-all
      extensible: <name of extensible predicate>
    data:
      - <tuple1>
      - <tuple2>
      - ...

用于 JavaScript 的 CodeQL 库公开了以下可扩展谓词

  • sourceModel(type, path, kind)
  • sinkModel(type, path, kind)
  • typeModel(type1, type2, path)
  • summaryModel(type, path, input, output, kind)

我们将解释如何使用这些谓词并提供一些示例,并在本文末尾提供一些参考资料。

示例:'execa' 包中的污点接收器

在本例中,我们将展示如何将传递给execa 的以下参数作为命令行注入接收器添加

import { shell } from "execa";
shell(cmd); // <-- add 'cmd' as a taint sink

请注意,此接收器已由 CodeQL JS 分析识别,但对于本示例,您可以使用以下数据扩展

extensions:
  - addsTo:
      pack: codeql/javascript-all
      extensible: sinkModel
    data:
      - ["execa", "Member[shell].Argument[0]", "command-injection"]
  • 由于我们添加了一个新的接收器,因此我们在sinkModel 可扩展谓词中添加了一个元组。
  • 第一列“execa”标识了一组值,从这些值开始搜索接收器。字符串“execa”表示我们从代码库导入 NPM 包execa 的位置开始。
  • 第二列是一个访问路径,它从左到右进行评估,从第一列标识的值开始。
    • Member[shell]选择对execa 包的shell 成员的访问。
    • Argument[0]选择对该成员的调用的第一个参数。
  • command-injection 表示这被认为是命令注入查询的接收器。

示例:来自 window 'message' 事件的污点源

在本例中,我们将展示如何将下面的event.data 表达式标记为远程流源

window.addEventListener("message", function (event) {
  let data = event.data; // <-- add 'event.data' as a taint source
});

请注意,此源已由 CodeQL JS 分析识别,但对于本示例,您可以使用以下数据扩展

extensions:
  - addsTo:
      pack: codeql/javascript-all
      extensible: sourceModel
    data:
      - [
          "global",
          "Member[addEventListener].Argument[1].Parameter[0].Member[data]",
          "remote",
        ]
  • 由于我们添加了一个新的污点源,因此我们在sourceModel 可扩展谓词中添加了一个元组。
  • 第一列“global”在对全局对象的引用(在浏览器上下文中也称为window)处开始搜索。这是一个特殊的 JavaScript 对象,它包含所有全局变量和方法。
  • Member[addEventListener]选择对addEventListener 成员的访问。
  • Argument[1]选择对该成员的调用的第二个参数(包含回调的参数)。
  • Parameter[0]选择回调的第一个参数(名为event 的参数)。
  • Member[data]选择对事件对象的data 属性的访问。
  • 最后,kind remote 表示这被认为是远程流的源。

在下一节中,我们将展示如何将模型限制为仅识别特定类型的事件。

示例续:限制事件类型

上面的模型将所有事件都视为远程流的源,而不仅仅是message 事件。例如,它还会选取以下无关源

window.addEventListener("onclick", function (event) {
  let data = event.data; // <-- 'event.data' became a spurious taint source
});

我们可以通过添加WithStringArgument 组件来细化模型,以限制正在考虑的调用集

extensions:
  - addsTo:
      pack: codeql/javascript-all
      extensible: sourceModel
    data:
      - [
          "global",
          "Member[addEventListener].WithStringArgument[0=message].Argument[1].Parameter[0].Member[data]",
          "remote",
        ]

这里的WithStringArgument[0=message] 组件选择对addEventListener 的调用子集,其中第一个参数是一个字符串文字,其值为“message”

示例:使用类型添加 MySQL 注入接收器

在本例中,我们将展示如何添加以下 SQL 注入接收器

import { Connection } from "mysql";

function submit(connection: Connection, q: string) {
  connection.query(q); // <-- add 'q' as a SQL injection sink
}

我们可以使用以下扩展来识别它

extensions:
  - addsTo:
      pack: codeql/javascript-all
      extensible: sinkModel
    data:
      - ["mysql.Connection", "Member[query].Argument[0]", "sql-injection"]
  • 第一列“mysql.Connection”从任何表达式开始搜索,这些表达式的值为已知是从mysql 包的Connection 类型实例化的。由于它的类型注释,这将选择上面的connection 参数。
  • Member[query]从连接对象中选择query 成员。
  • Argument[0]选择对该成员的调用的第一个参数。
  • sql-injection 表示这被认为是 SQL 注入查询的接收器。

这在本例中有效,因为connection 参数有一个与模型正在寻找的内容匹配的类型注释。

请注意,以下两行之间存在重大区别

data:
- ["mysql.Connection", "", ...]
- ["mysql", "Member[Connection]", ...]

第一行匹配mysql.Connection 的实例,它们是封装 MySQL 连接的对象。第二行将匹配诸如require('mysql').Connection 之类的内容,它本身不是连接对象。

在下一节中,我们将展示如何概括模型以处理没有类型注释的情况。

示例续:处理未类型化的代码

假设我们希望上面的模型检测到此代码段中的接收器

import { getConnection } from "@example/db";
let connection = getConnection();
connection.query(q); // <-- add 'q' as a SQL injection sink

connection 上没有类型注释,也没有指示getConnection() 返回什么。使用typeModel 元组,我们可以告诉我们的模型,此函数返回mysql.Connection 的实例

extensions:
  - addsTo:
      pack: codeql/javascript-all
      extensible: typeModel
    data:
      - ["mysql.Connection", "@example/db", "Member[getConnection].ReturnValue"]
  • 由于我们提供了类型信息,因此我们在typeModel 可扩展谓词中添加了一个元组。
  • 第一列“mysql.Connection”命名了我们正在添加新定义的类型。
  • 第二列“@example/db”在假设的 NPM 包@example/db 的导入处开始搜索。
  • Member[getConnection]选择对该包的getConnection 成员的引用。
  • ReturnValue选择对该成员的调用的返回值。

新模型指出getConnection() 的返回值类型为mysql.Connection。将此与我们之前添加的接收器模型相结合,示例中的接收器将被模型检测到。

这里使用的机制是库模型如何同时适用于 TypeScript 和纯 JavaScript 的方式。一个好的库模型包含typeModel 元组,以确保它即使在没有类型注释的代码库中也能正常工作。例如,包含在 CodeQL JS 分析中的mysql 模型包括此类型定义(在许多其他类型定义中)

- ["mysql.Connection", "mysql", "Member[createConnection].ReturnValue"]

示例:使用模糊模型简化建模

在本例中,我们将展示如何使用“模糊”模型添加以下 SQL 注入接收器

import * as mysql from 'mysql';
const pool = mysql.createPool({...});
pool.getConnection((err, conn) => {
  conn.query(q, (err, rows) => {...}); // <-- add 'q' as a SQL injection sink
});

我们可以使用模糊模型来识别它,如以下扩展所示

extensions:
  - addsTo:
      pack: codeql/javascript-all
      extensible: sinkModel
    data:
      - ["mysql", "Fuzzy.Member[query].Argument[0]", "sql-injection"]
  • 第一列“mysql”从导入 mysql 包的位置开始搜索。
  • Fuzzy 选择所有看起来源自 mysql 包的对象,例如 poolconnerrrows 对象。
  • Member[query] 从所有这些对象中选择query 成员。在本例中,唯一这样的成员是 conn.query。原则上,这也会找到诸如 pool.queryerr.query 之类的表达式,但在实践中,这样的表达式不太可能出现,因为 poolerr 对象没有名为 query 的成员。
  • Argument[0] 选择对所选成员的调用的第一个参数,即 conn.queryq 参数。
  • sql-injection 表示这被视为 SQL 注入查询的接收器。

为了参考,更详细的模型可能如下所示,如前面的示例所述

extensions:
  - addsTo:
      pack: codeql/javascript-all
      extensible: sinkModel
    data:
      - ["mysql.Connection", "Member[query].Argument[0]", "sql-injection"]

  - addsTo:
      pack: codeql/javascript-all
      extensible: typeModel
    data:
      - ["mysql.Pool", "mysql", "Member[createPool].ReturnValue"]
      - ["mysql.Connection", "mysql.Pool", "Member[getConnection].Argument[0].Parameter[1]"]

使用Fuzzy 组件的模型更简单,但代价是近似。当对大型或复杂的库进行建模时,此技术很有用,因为在这种情况下,很难编写详细的模型。

示例:添加通过 'decodeURIComponent' 的流

在本例中,我们将展示如何添加通过对 decodeURIComponent 的调用的流

let y = decodeURIComponent(x); // add taint flow from 'x' to 'y'

请注意,此流已由 CodeQL JS 分析识别,但对于本示例,您可以使用以下数据扩展

extensions:
  - addsTo:
      pack: codeql/javascript-all
      extensible: summaryModel
    data:
      - [
          "global",
          "Member[decodeURIComponent]",
          "Argument[0]",
          "ReturnValue",
          "taint",
        ]
  • 由于我们添加了通过函数调用的流,因此我们在summaryModel 可扩展谓词中添加了一个元组。
  • 第一列“global”从对全局对象的引用开始搜索相关调用。在 JavaScript 中,全局变量是全局对象的属性,因此这使我们能够访问全局变量或函数。
  • 第二列Member[decodeURIComponent] 是一个路径,它指向我们希望建模的函数调用。在本例中,我们选择了对全局对象的decodeURIComponent 成员的引用,即名为decodeURIComponent 的全局变量。
  • 第三列Argument[0] 指示流的输入。在本例中,它是对函数调用的第一个参数。
  • 第四列ReturnValue 指示流的输出。在本例中,它是函数调用的返回值。
  • 最后一列taint 指示要添加的流类型。值taint 表示输出不一定等于输入,但它是从输入中以污点保留的方式派生的。

示例:添加通过 'underscore.forEach' 的流

在本例中,我们将展示如何添加通过对underscore 包的forEach 的调用的流

require('underscore').forEach([x, y], (v) => { ... }); // add value flow from 'x' and 'y' to 'v'

请注意,此流已由 CodeQL JS 分析识别,但对于本示例,您可以使用以下数据扩展

extensions:
  - addsTo:
      pack: codeql/javascript-all
      extensible: summaryModel
    data:
      - [
          "underscore",
          "Member[forEach]",
          "Argument[0].ArrayElement",
          "Argument[1].Parameter[0]",
          "value",
        ]
  • 由于我们添加了通过函数调用的流,因此我们在summaryModel 可扩展谓词中添加了一个元组。
  • 第一列“underscore”从导入underscore 包的位置开始搜索相关调用。
  • 第二列Member[forEach] 选择对underscore 包的forEach 成员的引用。
  • 第三列指定流的输入
    • Argument[0] 选择forEach 的第一个参数,即正在迭代的数组。
    • ArrayElement 选择该数组的元素(表达式xy)。
  • 第四列指定流的输出
    • Argument[1] 选择forEach 的第二个参数(包含回调函数的参数)。
    • Parameter[0] 选择回调函数的第一个参数(名为v 的参数)。
  • 最后一列value 指示要添加的流类型。值value 表示输入值在流到输出时保持不变。

参考资料

以下部分提供可扩展谓词、访问路径、类型和种类的参考材料。

可扩展谓词

sourceModel(type, path, kind)

添加新的污点源。大多数污点跟踪查询将使用新的源。

  • type: 要评估path的类型的名称。
  • path: 导致源的访问路径。
  • kind: 要添加的源类型。目前仅使用remote

示例

extensions:
  - addsTo:
      pack: codeql/javascript-all
      extensible: sourceModel
    data:
      - ["global", "Member[user].Member[name]", "remote"]

sinkModel(type, path, kind)

添加新的污点接收器。接收器是特定于查询的,通常会影响一个或两个查询。

  • type: 要评估path的类型的名称。
  • path: 导致接收器的访问路径。
  • kind: 要添加的接收器类型。有关受支持类型的列表,请参阅接收器类型的部分。

示例

extensions:
  - addsTo:
      pack: codeql/javascript-all
      extensible: sinkModel
    data:
      - ["global", "Member[eval].Argument[0]", "code-injection"]

summaryModel(type, path, input, output, kind)

添加函数调用中的流。

  • type: 要评估path的类型的名称。
  • path: 导致函数调用的访问路径。
  • input: 相对于函数调用导致流输入的路径。
  • output: 相对于函数调用导致流输出的路径。
  • kind: 要添加的摘要类型。可以是taint表示污点传播流,或value表示值保留流。

示例

extensions:
  - addsTo:
      pack: codeql/javascript-all
      extensible: summaryModel
    data:
      - [
          "global",
          "Member[decodeURIComponent]",
          "Argument[0]",
          "ReturnValue",
          "taint",
        ]

typeModel(type1, type2, path)

添加类型的新定义。

  • type1: 要定义的类型的名称。
  • type2: 要评估path的类型的名称。
  • path: 从type2type1的访问路径。

示例

extensions:
- addsTo:
    pack: codeql/javascript-all
    extensible: typeModel
  data:
    - [
        "mysql.Connection",
        "@example/db",
        "Member[getConnection].ReturnValue",
      ]

类型

类型是一个字符串,用于标识一组值。在之前部分中提到的每个可扩展谓词中,第一列始终是类型的名称。可以通过为该类型添加typeModel元组来定义类型。此外,还提供了以下内置类型

  • NPM 包的名称与该包的导入相匹配。例如,类型express与表达式require(“express”)匹配。如果包名称包含点,则必须用单引号括起来,例如在‘lodash.escape’中。
  • 类型global标识全局对象,也称为window。在 JavaScript 中,全局变量是全局对象的属性,因此可以使用此类型标识全局变量。(此类型还与名为global的 NPM 包的导入匹配,该包恰好导出全局对象。)
  • 形式为<package>.<type>的限定类型名称标识来自<package>的类型为<type>的表达式。例如,mysql.Connection标识来自mysql包的类型为Connection的表达式。请注意,这仅在代码库中存在类型注释,或为该类型提供了足够的typeModel元组时才有效。

访问路径

pathinputoutput列包含一个.分隔的组件列表,该列表从左到右进行评估,每一步都会选择从之前的一组值派生的新一组值。

支持以下组件

  • Argument[number] 选择给定索引处的参数。
  • Argument[this] 选择方法调用的接收者。
  • Parameter[number] 选择给定索引处的参数。
  • Parameter[this] 选择函数的this参数。
  • ReturnValue 选择函数或调用的返回值。
  • Member[name] 选择具有给定名称的属性。
  • AnyMember 选择任何属性,无论名称如何。
  • ArrayElement 选择数组中的元素。
  • Element 选择数组、迭代器或集合对象的元素。
  • MapValue 选择映射对象的某个值。
  • Awaited 选择 promise 的值。
  • Instance 选择类的实例,包括其子类的实例。
  • Fuzzy 选择通过此列表中描述的其他操作组合派生的所有值。例如,这可以用于查找来自特定包的所有看起来源自该特定包的值。这对查找来自已知包的方法调用很有用,但接收者类型未知或难以建模。

以下组件称为“调用站点过滤器”。如果调用符合某些条件,它们会选择先前选择的调用子集

  • WithArity[number] 选择具有给定参数数量的调用子集。
  • WithStringArgument[number=value] 选择给定索引处的参数是具有给定值的字符串字面量的调用子集。

与装饰器相关的组件

  • DecoratedClass 选择一个类,该类具有当前值作为装饰器。例如,Member[Component].DecoratedClass 选择使用@Component装饰的任何类。
  • DecoratedParameter 选择由当前值装饰的参数。
  • DecoratedMember 选择由当前值装饰的方法、字段或访问器。

关于操作数语法的补充说明

  • 多个操作数可以被赋予单个组件,作为操作数联合的简写。例如,Member[foo,bar] 匹配Member[foo]Member[bar] 的联合。
  • ArgumentParameterWithArity 的数字操作数可以作为间隔给出。例如,Argument[0..2] 匹配参数 0、1 或 2。
  • Argument[N-1] 选择调用中的最后一个参数,而Parameter[N-1] 选择函数中的最后一个参数,N-2 是倒数第二个,依此类推。

种类

源种类

  • remote: 远程流的通用源。大多数污点跟踪查询将使用此类源。目前这是唯一受支持的源类型。

接收器种类

与源不同,接收器往往高度特定于查询,很少影响一个或两个以上的查询。并非每个查询都支持可自定义的接收器。如果以下接收器不适合您的用例,您应该添加新的查询。

  • code-injection: 可用于注入代码的接收器,例如在eval调用中。
  • command-injection: 可用于注入 shell 命令的接收器,例如在child_process.spawn调用中。
  • path-injection: 可用于文件系统访问中的路径注入的接收器,例如在fs.readFile调用中。
  • sql-injection: 可用于 SQL 注入的接收器,例如在 MySQL query 调用中。
  • nosql-injection: 可用于 NoSQL 注入的接收器,例如在 MongoDB findOne 调用中。
  • html-injection: 可用于 HTML 注入的接收器,例如在 jQuery $() 调用中。
  • request-forgery: 控制请求 URL 的接收器,例如在fetch调用中。
  • url-redirection: 可用于将用户重定向到恶意 URL 的接收器。
  • unsafe-deserialization: 反序列化接收器,可能导致代码执行或其他不安全行为,例如不安全的 YAML 解析器。
  • log-injection: 可用于日志注入的接收器,例如在console.log调用中。

摘要种类

  • taint: 传播污点的摘要。这意味着输出不一定等于输入,但它是以不受限制的方式从输入派生的。控制输入的攻击者也将对输出有很大的控制权。
  • value: 保留输入的值或创建输入副本的摘要,以便保留其所有对象属性。
  • ©GitHub, Inc.
  • 条款
  • 隐私