Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,6 @@ node_modules/

# Output of 'npm pack'
*.tgz

# corpus files in the path traversal example except for manual test.zip
examples/bug-detectors/path-traversal/corpus/
7 changes: 4 additions & 3 deletions docs/fuzz-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,10 @@ configuration. For example, to disable all built-in bug detectors, add

Following built-in bug detectors are available in Jazzer.js:

| Bug Detector | Description |
| ------------------- | ----------------------------------------------------------- |
| `command-injection` | Hooks all functions of the built-in module `child_process`. |
| Bug Detector | Description |
| ------------------- | -------------------------------------------------------------------- |
| `command-injection` | Hooks all functions of the built-in module `child_process`. |
| `path-traversal` | Hooks all relevant functions of the built-in modules `fs` and `path` |

For implementation details see
[../packages/bug-detectors/internal](../packages/bug-detectors/internal).
Expand Down
Binary file not shown.
45 changes: 45 additions & 0 deletions examples/bug-detectors/path-traversal/fuzz.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2023 Code Intelligence GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// eslint-disable-next-line @typescript-eslint/no-var-requires
const JSZip = require("jszip");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require("path");

/**
* This demonstrates the path traversal bug detector on a vulnerable version of jszip.
* @param { Buffer } data
*/
module.exports.fuzz = function (data) {
// Parse the buffer into a JSZip object. The buffer might have been obtained from an http-request.
// See https://stuk.github.io/jszip/documentation/howto/read_zip.html for some examples.
return (
JSZip.loadAsync(data)
.then((zip) => {
for (const file in zip.files) {
// We might want to extract the file from the zip archive and write it to disk.
// The loadAsync function should have sanitized the path already.
// Here we only construct the absolute path and trigger the path traversal bug.
// This issue was fixed in jszip 3.8.0.
path.join(__dirname, file);
}
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.catch(() => {
/* ignore broken zip files */
})
);
};
16 changes: 16 additions & 0 deletions examples/bug-detectors/path-traversal/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "custom-hooks-bd",
"version": "1.0.0",
"main": "fuzz.js",
"license": "ISC",
"dependencies": {
"jszip": "3.7.1"
},
"scripts": {
"fuzz": "jazzer fuzz -i fuzz.js -i jszip corpus -- -runs=10000000 -print_final_stats=1 -use_value_profile=1 -max_len=600 -seed=123456789",
"dryRun": "jazzer fuzz --sync -x Error -- -runs=100000 -seed=123456789"
},
"devDependencies": {
"@jazzer.js/core": "file:../../packages/core"
}
}
3 changes: 2 additions & 1 deletion fuzztests/.jazzerjsrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"fuzzerOptions": ["-use_value_profile=1", "-runs=100000"],
"includes": ["jazzer.js"],
"timeout": 1000
"timeout": 1000,
"disableBugDetectors": ['path-traversal']
}
6 changes: 2 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 1 addition & 6 deletions packages/bug-detectors/findings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Examples showcasing the custom hooks API
*/

export class Finding extends Error {
constructor(message: string) {
super(message);
}
}
export class Finding extends Error {}

// The first finding found by any bug detector will be saved here.
// This is a global variable shared between the core-library (read, reset) and the bug detectors (write).
Expand Down
3 changes: 3 additions & 0 deletions packages/bug-detectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,7 @@ export async function loadBugDetectors(
if (!shouldDisableBugDetector(disableBugDetectors, "command-injection")) {
await import("./internal/command-injection.js");
}
if (!shouldDisableBugDetector(disableBugDetectors, "path-traversal")) {
await import("./internal/path-traversal.js");
}
}
201 changes: 201 additions & 0 deletions packages/bug-detectors/internal/path-traversal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*
* Copyright 2023 Code Intelligence GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { reportFinding } from "../findings";
import { guideTowardsContainment } from "@jazzer.js/fuzzer";
import { callSiteId, registerBeforeHook } from "@jazzer.js/hooking";

/**
* Importing this file adds "before-hooks" for all functions in the built-in `fs`, `fs/promises`, and `path` module and guides
* the fuzzer towards the uniquely chosen `goal` string `"../../jaz_zer"`. If the goal is found in the first argument
* of any hooked function, a `Finding` is reported.
*/
const goal = "../../jaz_zer";
const modulesToHook = [
{
moduleName: "fs",
functionNames: [
"access",
"accessSync",
"appendFile",
"appendFileSync",
"chmod",
"chown",
"chownSync",
"chmodSync",
"createReadStream",
"createWriteStream",
"exists",
"existsSync",
"lchmod",
"lchmodSync",
"lchown",
"lchownSync",
"lstat",
"lstatSync",
"lutimes",
"lutimesSync",
"mkdir",
"mkdirSync",
"open",
"opendir",
"opendirSync",
"openAsBlob",
"openSync",
"readFile",
"readFileSync",
"readlink",
"readlinkSync",
"readdir",
"readdirSync",
"realpath",
"realpathSync",
"rm",
"rmSync",
"rmdir",
"rmdirSync",
"stat",
"statfs",
"statfsSync",
"statSync",
"truncate",
"truncateSync",
"unlink",
"unlinkSync",
"unwatchFile",
"utimes",
"utimesSync",
"watch",
"watchFile",
"writeFile",
"writeFileSync",
],
},
{
moduleName: "fs/promises",
functionNames: [
"access",
"appendFile",
"chmod",
"chown",
"lchmod",
"lchown",
"lstat",
"lutimes",
"mkdir",
"open",
"opendir",
"readFile",
"readlink",
"readdir",
"realpath",
"rm",
"rmdir",
"stat",
"statfs",
"truncate",
"unlink",
"utimes",
"watch",
"writeFile",
],
},
// path.join() can have any number of strings as inputs. Internally, it uses path.normalize(), which we hook here.
{
moduleName: "path",
functionNames: ["normalize", "resolve"],
},
];

for (const module of modulesToHook) {
for (const functionName of module.functionNames) {
const beforeHook = (
thisPtr: unknown,
params: unknown[],
hookId: number
) => {
if (params === undefined || params.length === 0) {
return;
}
// The first argument of the original function is typically
// a path or a file name.
const firstArgument = params[0] as string;
if (firstArgument.includes(goal)) {
reportFinding(
`Path Traversal in ${functionName}(): called with '${firstArgument}'`
);
}
guideTowardsContainment(firstArgument, goal, hookId);
};

registerBeforeHook(functionName, module.moduleName, false, beforeHook);
}
}

// Some functions have two arguments that can be used for path traversal.
const functionsWithTwoTargets = [
{
moduleName: "fs/promises",
functionNames: ["copyFile", "cp", "link", "rename", "symlink"],
},
{
moduleName: "fs",
functionNames: [
"copyFile",
"copyFileSync",
"cp",
"cpSync",
"link",
"linkSync",
"rename",
"renameSync",
"symlink",
"symlinkSync",
],
},
];

for (const module of functionsWithTwoTargets) {
for (const functionName of module.functionNames) {
const makeBeforeHook = (extraHookId: number) => {
return (thisPtr: unknown, params: unknown[], hookId: number) => {
if (params === undefined || params.length < 2) {
return;
}
// The first two arguments are paths.
const firstArgument = params[0] as string;
const secondArgument = params[1] as string;
if (firstArgument.includes(goal) || secondArgument.includes(goal)) {
reportFinding(
`Path Traversal in ${functionName}(): called with '${firstArgument}'` +
` and '${secondArgument}'`
);
}
guideTowardsContainment(firstArgument, goal, hookId);
// We don't want to confuse the fuzzer guidance with the same hookId for both function arguments.
// Therefore, we use an extra hookId for the second argument.
guideTowardsContainment(secondArgument, goal, extraHookId);
};
};

registerBeforeHook(
functionName,
module.moduleName,
false,
makeBeforeHook(callSiteId(functionName, module.moduleName, "secondId"))
);
}
}
3 changes: 1 addition & 2 deletions packages/bug-detectors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
"main": "dist/index.js",
"types": "dist/index.d.js",
"dependencies": {
"@jazzer.js/fuzzer": "*",
"@jazzer.js/instrumentor": "*"
"@jazzer.js/fuzzer": "*"
},
"devDependencies": {},
"engines": {
Expand Down
7 changes: 3 additions & 4 deletions packages/core/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ yargs(process.argv.slice(2))
"A list of patterns to disable internal bug detectors. By default all internal " +
"bug detectors are enabled. To disable all, use the '.*' pattern." +
"Following bug detectors are available: " +
" command-injection",
" command-injection\n" +
" path-traversal\n",
type: "string",
group: "Fuzzer:",
default: [],
Expand All @@ -224,9 +225,7 @@ yargs(process.argv.slice(2))
coverage: args.cov,
coverageDirectory: args.cov_dir,
coverageReporters: args.cov_reporters,
disableBugDetectors: args.disable_bug_detectors.map(
(s: string) => new RegExp(s)
),
disableBugDetectors: args.disable_bug_detectors,
});
}
)
Expand Down
Loading