CI Fuzz JavaScript/TypeScript tutorial
This tutorial intends to teach you the basics of using cifuzz
with a JavaScript/TypeScript
project. cifuzz
directly supports npx.
This guide was created using Ubuntu 20.04 64-bit. However, if you are on MacOS or Windows, you should still be able to follow along without any issues.
If you haven't yet installed cifuzz
, then first head to the
installation section
cifuzz supports only NodeJS applications because that's a Jazzer.js limitation.
1. Setting up the project
Download or clone the following repository:
ci-fuzz-cli-tutorials.
The above repository contains several projects that highlight the usage of cifuzz
.
Please change into the js_ts/nodejs-js
directory for this tutorial and try to follow along, by running all highlighted cifuzz
commands listed below.
Please change into the js_ts/nodejs-ts
directory for the TypeScript application.
2. Initializing the project
The first step for any project is to initialize it. Initialization entails
creating a cifuzz.yaml
configuration file in the project directory. This step does not require any manual work. Please run the following command:
cifuzz init
When running cifuzz init
, the tool recognizes the nature of the project. In this case, a NodeJS project, and provides dependency hints for you that you need to install. Your top level package.json
file will be modified by running the given commands for JavaScript and TypeScript applications respectively.
JavaScript
For the JavaScript application, the following commands need to be executed depending on the package manager of your choice.
npm install --save-dev @jazzer.js/jest-runner @jazzer.js/core
or
yarn add --dev @jazzer.js/jest-runner @jazzer.js/core
You also need to add the following code snippet to your jest.config.js
:
module.exports = {
projects: [
{
displayName: "test",
},
{
runner: "@jazzer.js/jest-runner",
displayName: {
name: "Jazzer.js",
color: "cyan",
},
testMatch: ["<rootDir>/**/*.fuzz.js"],
},
],
};
TypeScript
For the TypeScript application, the following commands need to be executed depending on the package manager of your choice.
npm install --save-dev @jazzer.js/jest-runner ts-jest @jazzer.js/core
or
yarn add --dev @jazzer.js/jest-runner ts-jest @jazzer.js/core
You also need to add the following code snippet to your jest.config.js
:
import type { Config } from "jest";
const config: Config = {
verbose: true,
projects: [
{
displayName: "Jest",
preset: "ts-jest",
},
{
displayName: {
name: "Jazzer.js",
color: "cyan",
},
preset: "ts-jest",
runner: "@jazzer.js/jest-runner",
testEnvironment: "node",
testMatch: ["<rootDir>/*.fuzz.[jt]s"],
},
],
coveragePathIgnorePatterns: ["/node_modules/", "/dist/"],
modulePathIgnorePatterns: ["/node_modules", "/dist/"],
};
export default config;
Additionally, for TypeScript applications adding the following line to globals.d.ts
file introduces the fuzz test function types globally.
import "@jazzer.js/jest-runner";
Initializtion continued
jazzer.js/core
- this dependency will install Jazzer.js and all required dependecies.jazzer.js/jest-runner
- this dependency enablescifuzz
and Jest integration with your project.
After installing all shown dependencies, your package.json
needs to look identical to:
{
"name": "TODO list server",
"devDependencies": {
"@jazzer.js/core": "^1.6.1",
"@jazzer.js/jest-runner": "^1.6.1",
...
}
},
3. Creating a fuzz test
The following sections are based on the JavaScript application. However, they are analogous for the TypeScript application.
The next step is to create a JavaScript fuzz test template. With one of the main goals
of cifuzz
being to make fuzz testing as easy as unit testing, you create one similar as
you would do in a standard unit test. Run the following command:
cifuzz create js -o CommandExecution.fuzz.js
Opening CommandExecution.fuzz.js
yields a basic fuzz test template right away.
Before extending the template fuzz test, take a look at the target method that you want to fuzz. It's located in utils.js
:
/**
* @param { string } command
*/
function commandExecution(command, fn) {
child_process.exec(command, (err, stdout, stderr) => {
fn(stdout)
});
}
The main part to focus on here is the parameter that the commandExecution
method
requires, string command
. As long as you can pass the correct data types to the
function, the fuzzer takes care of the rest.
Equipped with that knowledge, writing the fuzz test is next. You can add or copy/paste the following into
CommandExecution.fuzz.js
:
const { FuzzedDataProvider } = require("@jazzer.js/core");
const utils = require('./utils');
test.fuzz("My fuzz test", (data) => {
if (data.length <= 1) {
return;
}
const provider = new FuzzedDataProvider(data);
var command = provider.consumeString(30);
try {
utils.commandExecution(command, (_data) => {})
}catch(e) {
}
});
For this fuzz test we also considered to not send string with 0 length.
A couple of important notes regarding the fuzz test:
- The fuzz test file needs to follow the name schema
<test_file_name>.fuzz.js
- The fuzz test must import
@jazzer.js/core
. - The above fuzz test uses the
FuzzedDataProvider
class. This isn't required, but it's a convenient way to split the fuzzing input in thedata
variable into different native data types that are easier to use than the raw bytes provided bydata
. Here is a link about FuzzedDataProvider in Jazzer.js. - Once you have prepared all required arguments for the function call you want to fuzz, call it. In the above scenario we built
command
using data from the fuzzer, the fuzz test has to call the target methodcommandExecution
with the fuzz data.
4. Executing the fuzz test
You configured everything and created a fuzz test. Now, run the fuzz test using the fuzz harness name:
cifuzz run CommandExecution
Shortly after executing the fuzz test you will get a notification that cifuzz
discovered a bug in commandExecution
. Here is a snippet of the output:
💥 [giggling_tiger] Command Injection in exec(): called with 'jaz_zer/' in reportFinding (node_modules/@jazzer.js/bug-detectors/findings.ts:45:17)
5. Examining a finding
When cifuzz
discovers a finding, it stores the output from the finding and the
input that caused it. You can list all findings discovered so far by running
cifuzz findings
. If you want to see the details of a specific finding just
provide its name, for example cifuzz finding giggling_tiger
. This provides the
stack trace and other details about the finding that helps you debug and fix the
issue. Examining the output from cifuzz finding giggling_tiger
below shows that
there was a command injection based on externally controlled data.
This was triggered at line 109 in the utils.js
.
[giggling_tiger] Command Injection in exec(): called with 'jaz_zer/' in reportFinding (node_modules/@jazzer.js/bug-detectors/findings.ts:45:17)
Date: 2023-07-28 16:19:40.778791579 +0200 CEST
● My fuzz test
Command Injection in exec(): called with 'jaz_zer/'
107 |
108 | function commandExecution(command, fn) {
> 109 | child_process.exec(command, (err, stdout, stderr) => {
| ^
110 | fn(stdout)
111 | });
112 | }
at reportFinding (node_modules/@jazzer.js/bug-detectors/findings.ts:45:17)
at Hook.beforeHook [as hookFunction] (node_modules/@jazzer.js/bug-detectors/internal/command-injection.ts:50:17)
at Object.exec (node_modules/@jazzer.js/hooking/manager.ts:240:10)
at Object.commandExecution (utils.js:109:19)
at CommandExecution.fuzz.js:14:15
at node_modules/@jazzer.js/core/core.ts:411:15
If you examine line 109 in utils.js
, you can observe that this is the location where execution attempts to call the exec
function.
cifuzz
also stores the crashing input in a .cifuzz-findings directory. In this
example, that would be .cifuzz-findings/giggling_tiger
. This can be helpful when
debugging your app.