OSS-Fuzz 接入 JavaScript / Node.js 项目实战指南:从 project.yaml 到 Jazzer.js Fuzz Target 的完整落地
【免费下载链接】oss-fuzzOSS-Fuzz - continuous fuzzing for open source software.项目地址: https://gitcode.com/gh_mirrors/os/oss-fuzz
本文以 OSS-Fuzz 官方文档 docs/getting-started/new-project-guide/javascript_lang.md 为主线,系统讲解如何在 OSS-Fuzz 中为 JavaScript(Node.js)及可转译为 JavaScript 的项目(如 TypeScript)搭建持续模糊测试(fuzzing)工程。你将掌握project.yaml、Dockerfile、build.sh、fuzz target 编写与FuzzedDataProvider使用的完整流程,并了解 Jazzer.js 在底层如何驱动 libFuzzer 执行模糊测试。读完即可参照 projects/javascript-example 与 projects/typescript-example 两个官方示例项目,把自有 JS 项目接入 OSS-Fuzz。
JavaScript 项目接入 OSS-Fuzz 的整体流程
将 JavaScript(Node.js)项目集成进 OSS-Fuzz 的过程,与通用的 Setting up a new project 流程高度相似:同样需要提供project.yaml(项目元数据)、Dockerfile(构建环境)、build.sh(构建脚本)以及若干 fuzz target(模糊测试入口)。JavaScript 项目的关键差异集中体现在三处:
- 语言与引擎选择:
language必须声明为javascript,fuzzing engine 仅支持libFuzzer,sanitizer 使用none; - 基础镜像:Dockerfile 以
gcr.io/oss-fuzz-base/base-builder-javascript为起点,镜像内预装 Node.js 与npm; - 模糊测试引擎:JS 模糊测试由 Jazzer.js 驱动,它工作在 JavaScript 源码层面,因此对任何可转译为 JavaScript 的语言(如 TypeScript)都适用。
下文将按照 "环境 → 配置 → 编写 fuzz target → 构建 → 数据提供器" 的顺序逐步展开。
Jazzer.js:JavaScript 模糊测试引擎
OSS-Fuzz 中的 JavaScript 模糊测试由 Jazzer.js 提供能力,该引擎在构建阶段由基础镜像自动安装。
与基于 JVM 的 Jazzer(Java)不同,Jazzer.js 直接作用于JavaScript 源码层面(source-code level),这意味着:
- 它不需要把被测代码编译成特殊格式,普通 Node.js 模块即可被 fuzz;
- 任何能转译为 JavaScript 的语言(典型如 TypeScript)都能复用同一套 fuzz 流程;
- fuzz target 以普通 Node.js 模块形式存在,导出名为
fuzz的函数即可,fuzz target 的具体形态可参考 Jazzer.js 官方的 Usage 文档。
Jazzer.js 在底层依赖 libFuzzer 的 native addon 完成覆盖率引导(coverage-guided)的变异与反馈,同时负责将命令行参数转发给该 addon——这正是compile_javascript_fuzzer生成的包装脚本能"无缝替换 libFuzzer"的原因(详见下文 build.sh 一节)。
官方示例项目:从零理解文件组织
仓库内提供了两个可直接对照学习的示例项目:
| 示例 | 路径 | 说明 |
|---|---|---|
| JavaScript 示例 | projects/javascript-example | 最简单的 JS 模糊测试工程,包含 3 个 fuzz target 与完整构建脚本 |
| TypeScript 示例 | projects/typescript-example | 展示 TS 项目的接入方式,并演示了FuzzedDataProvider的用法 |
javascript-example的完整文件清单如下:
project.yaml:项目元数据(语言、引擎、sanitizer);Dockerfile:基于base-builder-javascript镜像,把 fuzz target 拷贝进$SRC/example;package.json:npm 依赖描述;fuzz_string_compare.js、fuzz_promise.js、fuzz_value_profiling.js:三个不同风格的 fuzz target;build.sh:依赖安装与 fuzzer 构建脚本。
typescript-example则额外包含tsconfig.json、target.ts与fuzz_explore_me.ts,展示 TypeScript 源码经编译后进入 fuzz 流程的标准做法。
project.yaml:语言、引擎与 Sanitizer 声明
JavaScript 项目的project.yaml中,language属性必须声明为:
language: javascript引擎与 sanitizer 的约束如下:
- fuzzing engine 仅支持 libFuzzer(
libfuzzer),目前没有其他可选项; - 原生 sanitizer(如 AddressSanitizer
address、UndefinedBehaviorSanitizerundefined)暂不支持。这类 sanitizer 只在项目包含原生插件(native addons)时才需要,而这对 JavaScript 项目来说属于较少见的情况。如果你确实需要 ASan 或 UBSan,官方建议在 Jazzer.js 仓库提交 issue 提出需求; none是 JavaScript 项目的默认 sanitizer,因此在project.yaml中显式书写是可选的,但建议显式声明以增强可读性。
一个完整的project.yaml配置如下(取自 projects/javascript-example/project.yaml,typescript-example 与之相同):
homepage: https://github.com/CodeIntelligenceTesting/jazzer.js language: javascript main_repo: https://github.com/CodeIntelligenceTesting/jazzer.js fuzzing_engines: - libfuzzer sanitizers: - none vendor_ccs: - yakdan@code-intelligence.com - norbert.schneider@code-intelligence.com - peter.samarin@code-intelligence.com其中vendor_ccs用于声明该项目模糊测试的维护联系人(本项目为 Jazzer.js 团队),实际接入你自己的项目时,应替换为对应的维护者邮箱。
Dockerfile:基于 base-builder-javascript 搭建构建环境
JavaScript 项目的Dockerfile必须以基础镜像开头:
FROM gcr.io/oss-fuzz-base/base-builder-javascript该 OSS-Fuzz 基础镜像已预装 Node.js 19 与npm,因此通常无需再安装 Node 运行时。镜像内还预装了compile_javascript_fuzzer构建脚本(详见下文)。Dockerfile 中通常只需要:
- 克隆目标项目源码(或像示例那样把 fuzz target 直接拷贝进镜像);
- 设置
WORKDIR; - 按需安装项目特定依赖、拷贝必要文件。
以 projects/javascript-example/Dockerfile 为模板:
FROM gcr.io/oss-fuzz-base/base-builder-javascript COPY build.sh $SRC/ # For real projects, you would clone your repo in the next step. RUN mkdir -p $SRC/example # Ideally, you have already configured fuzz tests in your repo so that they # run (in Jazzer.js regression mode) as part of unit testing. Keeping the fuzz # tests in sync with the source code ensures that they are adjusted continue # to work after code changes. Here, we copy them into the example project directory. COPY fuzz_string_compare.js fuzz_promise.js fuzz_value_profiling.js package.json $SRC/example/ WORKDIR $SRC/example注意示例注释中强调的最佳实践:建议把 fuzz test 与源码放在同一仓库中,并让它们在单元测试阶段以 Jazzer.js 回归模式(regression mode)运行,这样当源码变更导致 fuzz target 失效时能被及时发现,保持 fuzz 测试与代码同步演进。
Fuzz Target:最简单的形态是导出一个 fuzz 函数
在最简单的情况下,每个 fuzzer 就是一个导出了名为fuzz的函数的单个 JavaScript 文件,该函数接收一个参数,类型为 Node.js 的 Buffer。
以下是一个名为fuzz_string_compare.js的示例 fuzz target(与 projects/javascript-example/fuzz_string_compare.js 内容一致):
/** * @param { Buffer } data */ module.exports.fuzz = function (data) { const s = data.toString(); if (s.length !== 16) { return; } if ( s.slice(0, 8) === "Awesome " && s.slice(8, 15) === "Fuzzing" && s[15] === "!" ) { throw Error("Welcome to Awesome Fuzzing!"); } };这个 target 演示了模糊测试的核心机制:fuzzer 不断生成随机输入并调用fuzz(data),一旦输入恰好满足s === "Awesome Fuzzing!"这一串条件,代码就会抛出Error。Jazzer.js 基于 libFuzzer 的覆盖率反馈会逐步引导变异方向,最终"发现"这条路径,从而触发一个可被报告的崩溃(crash)。
异步 fuzz target:基于 Promise 的写法
JavaScript 生态大量使用异步 API,因此 fuzz target 也可以返回 Promise。示例 projects/javascript-example/fuzz_promise.js 展示了这一点:它在setTimeout回调中读取三个字节,若满足one + two + three === 42则 reject 一个 Error,并额外校验了异步调用的执行顺序:
let lastInvocationCount = 0; let invocationCount = lastInvocationCount + 1; /** * @param { Buffer } data */ module.exports.fuzz = function (data) { return new Promise((resolve, reject) => { if (data.length < 3) { resolve(invocationCount++); return; } setTimeout(() => { let one = data.readInt8(0); let two = data.readInt8(1); let three = data.readInt8(2); if (one + two + three === 42) { reject( new Error( `${one} + ${two} + ${three} = 42 (invocation ${invocationCount})` ) ); } else { resolve(invocationCount++); } }, 10); }).then((value) => { if (value !== lastInvocationCount + 1) { throw new Error( `Invalid invocation order, received ${value} but last invocation was ${lastInvocationCount}.` ); } lastInvocationCount = value; }); };使用 Buffer 原生方法的数值解析 target
projects/javascript-example/fuzz_value_profiling.js 展示了直接用data.readInt32BE()从 Buffer 中读取大端 32 位整数,并对结果做 XOR 校验后抛错的模式:
/** * @param {number} n */ function encrypt(n) { return n ^ 0x11223344; } /** * @param { Buffer } data */ module.exports.fuzz = function (data) { if (data.length < 16) { return; } if ( encrypt(data.readInt32BE(0)) === 0x50555637 && encrypt(data.readInt32BE(4)) === 0x7e4f5664 && encrypt(data.readInt32BE(8)) === 0x5757493e && encrypt(data.readInt32BE(12)) === 0x784c5465 ) { throw Error("XOR with a constant is not a secure encryption method ;-)"); } };该 target 还带有--sync构建参数的使用场景(同步模式,详见下文 build.sh)。
build.sh:借助 compile_javascript_fuzzer 一键构建
OSS-Fuzz 的 JavaScript 基础镜像预装了compile_javascript_fuzzer脚本。在build.sh中,你需要:
- 安装项目依赖;
- 如有必要,把 TypeScript 等语言编译为 JavaScript;
- 调用
compile_javascript_fuzzer构建各个 fuzzer。
compile_javascript_fuzzer脚本承担两项关键职责:
- 确保
@jazzer.js/core已安装,从而可以使用其 CLI 执行 fuzz 测试; - 生成一个 libFuzzer 的"即插即用"包装脚本——生成的脚本接受与 libFuzzer 相同的命令行参数,底层只是把这些参数转发给 Jazzer.js 所用的 libFuzzer native addon。
以 javascript-example 的 projects/javascript-example/build.sh 为例:
#!/bin/bash -eu # Install dependencies. npm install # Install Jazzer.js npm install --save-dev @jazzer.js/core # Build Fuzzers. compile_javascript_fuzzer example fuzz_promise.js compile_javascript_fuzzer example fuzz_string_compare.js --sync compile_javascript_fuzzer example fuzz_value_profiling.js --sync其中compile_javascript_fuzzer的参数含义如下:
| 参数 | 含义 |
|---|---|
| 第 1 个参数 | 项目在$SRC目录下的相对路径(如example) |
| 第 2 个参数 | 项目内 fuzz test 文件的相对路径(如fuzz_string_compare.js) |
| 其余参数 | 原样转发给 Jazzer.js CLI(如--sync表示同步模式执行) |
--sync标志指示 Jazzer.js 以同步方式执行 fuzz target,适用于不依赖异步操作的简单 target;对于返回 Promise 的异步 target(如fuzz_promise.js),则不加该标志,让 Jazzer.js 处理异步完成。构建完成后,OSS-Fuzz 的回归(regression)与持续模糊测试阶段会直接运行这些生成的 libFuzzer 包装脚本。
FuzzedDataProvider:把原始字节翻译成 JS 原生类型
Jazzer.js 提供了FuzzedDataProvider,它的作用是把 fuzzer 输入的原始字节流翻译成便于使用的 JavaScript 原始类型,从而显著简化 fuzz target 的编写。其功能与 Java、C++ 等其他语言中的FuzzedDataProvider类似(C++ 侧的通用思路可参考 Google 的 split-inputs 文档)。
使用FuzzedDataProvider的 fuzz target 写法如下:
const { FuzzedDataProvider } = require("@jazzer.js/core"); /** * @param { Buffer } fuzzerInputData */ module.exports.fuzz = function (fuzzerInputData) { const data = new FuzzedDataProvider(fuzzerInputData); const i = data.consumeIntegral(4); const s = data.consumeRemainingAsString(); exploreMe(i, s); };代码要点:
- 从
@jazzer.js/core中解构引入FuzzedDataProvider; - 用 fuzzer 传入的 Buffer 构造 provider 实例;
consumeIntegral(4)从输入中消费一个 4 字节整数;consumeRemainingAsString()把剩余输入整体消费为字符串;- 随后用这两个"有意义"的值调用被测函数
exploreMe(i, s)。
这正是 projects/typescript-example 中fuzz_explore_me.ts所演示的模式:TypeScript 项目把 fuzz target 写成.ts文件,构建时编译为 JavaScript 后同样通过compile_javascript_fuzzer构建。该示例也证明了所有可转译为 JavaScript 的语言都能无缝复用这套 fuzz 流程。
实战自查清单
完成 JavaScript 项目接入后,可用以下清单快速自查:
project.yaml中language: javascript、fuzzing_engines: [libfuzzer]、sanitizers: [none]是否齐全;Dockerfile是否以FROM gcr.io/oss-fuzz-base/base-builder-javascript开头;- fuzz target 是否导出了
module.exports.fuzz = function (data) { ... },且参数类型为 Buffer; build.sh是否依次完成npm install、安装@jazzer.js/core、调用compile_javascript_fuzzer;- 是否按 target 的同步/异步特性正确使用
--sync标志; - 复杂输入解析是否优先考虑
FuzzedDataProvider(consumeIntegral、consumeRemainingAsString等)以提升命中率。
将上述文件放入projects/<your-project>/目录并提交后,即可按 OSS-Fuzz 的通用项目提交流程申请接入,由 OSS-Fuzz 基础设施持续运行模糊测试并报告崩溃。
【免费下载链接】oss-fuzzOSS-Fuzz - continuous fuzzing for open source software.项目地址: https://gitcode.com/gh_mirrors/os/oss-fuzz
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考