Skip to content

Commit 11c4ba3

Browse files
committed
feat(path-traversal): add finding suppression rules
Expose path-traversal.ignore(rule) with the same file, function, and stack matching semantics as code injection. Emit copy-paste suppression snippets in findings, and cover the new rules in the integration tests and detector docs.
1 parent 709c702 commit 11c4ba3

6 files changed

Lines changed: 224 additions & 2 deletions

File tree

docs/bug-detectors.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,24 @@ using Jest in `.jazzerjsrc.json`:
2525
Hooks all relevant functions of the built-in modules `fs` and `path` and reports
2626
a finding if the fuzzer could pass a special path to any of the functions.
2727

28+
The Path Traversal bug detector can be configured in the
29+
[custom hooks](./fuzz-settings.md#customhooks--arraystring) file.
30+
31+
- `ignore(rule)` - suppresses findings matching a file, function, or stack
32+
pattern. Multiple properties in one rule are matched as a logical AND.
33+
34+
Here is an example configuration in the
35+
[custom hooks](./fuzz-settings.md#customhooks--arraystring) file:
36+
37+
```javascript
38+
const { getBugDetectorConfiguration } = require("@jazzer.js/bug-detectors");
39+
40+
getBugDetectorConfiguration("path-traversal")?.ignore({
41+
filePattern: /src[\\/]safe-path-wrapper\.js$/,
42+
functionPattern: /^sanitizePath$/,
43+
});
44+
```
45+
2846
_Disable with:_ `--disableBugDetectors=path-traversal` in CLI mode; or when
2947
using Jest in `.jazzerjsrc.json`:
3048

packages/bug-detectors/internal/path-traversal.ts

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,55 @@ import {
2020
} from "@jazzer.js/core";
2121
import { callSiteId, registerBeforeHook } from "@jazzer.js/hooking";
2222

23+
import { bugDetectorConfigurations } from "../configuration";
24+
import {
25+
buildSuppressionSnippet,
26+
captureStack,
27+
getRelevantStackLines,
28+
matchesIgnoreRules,
29+
parseOriginFrame,
30+
type IgnoreRule,
31+
type OriginFrame,
32+
} from "./finding-suppression";
33+
2334
/**
2435
* Importing this file adds "before-hooks" for all functions in the built-in `fs`, `fs/promises`, and `path` module and guides
2536
* the fuzzer towards the uniquely chosen `goal` string `"../../jaz_zer"`. If the goal is found in the first argument
2637
* of any hooked function, a `Finding` is reported.
2738
*/
2839
const goal = "../../jaz_zer";
40+
const MAX_STACK_LINES = 8;
41+
42+
export type { IgnoreRule } from "./finding-suppression";
43+
44+
/**
45+
* Configuration for the Path Traversal bug detector.
46+
* Controls suppression of matched path traversal findings.
47+
*/
48+
export interface PathTraversalConfig {
49+
/**
50+
* Suppresses findings that match the provided rule.
51+
* Use this to silence known-safe paths in your test environment.
52+
*/
53+
ignore(rule: IgnoreRule): this;
54+
}
55+
56+
class PathTraversalConfigImpl implements PathTraversalConfig {
57+
private readonly _ignoredRules: IgnoreRule[] = [];
58+
59+
ignore(rule: IgnoreRule): this {
60+
this._ignoredRules.push(rule);
61+
return this;
62+
}
63+
64+
shouldReport(originFrame: OriginFrame | undefined, stack: string): boolean {
65+
return !matchesIgnoreRules(this._ignoredRules, originFrame, stack);
66+
}
67+
}
68+
69+
const config = new PathTraversalConfigImpl();
70+
bugDetectorConfigurations.set("path-traversal", config);
71+
2972
const modulesToHook = [
3073
{
3174
moduleName: "fs",
@@ -208,11 +251,42 @@ function detectFindingAndGuideFuzzing(
208251
) {
209252
const argument = input.toString();
210253
if (argument.includes(goal)) {
254+
const stack = captureStack();
255+
const originFrame = parseOriginFrame(stack);
256+
if (!config.shouldReport(originFrame, stack)) {
257+
return;
258+
}
211259
reportAndThrowFinding(
212-
"Path Traversal\n" +
213-
` in ${functionName}(): called with '${argument}'`,
260+
buildFindingMessage(functionName, argument, stack, originFrame),
261+
false,
214262
);
215263
}
216264
guideTowardsContainment(argument, goal, hookId);
217265
}
218266
}
267+
268+
function buildFindingMessage(
269+
functionName: string,
270+
argument: string,
271+
stack: string,
272+
originFrame: OriginFrame | undefined,
273+
): string {
274+
const relevantStackLines = getRelevantStackLines(stack).slice(
275+
0,
276+
MAX_STACK_LINES,
277+
);
278+
const message = [
279+
"Path Traversal",
280+
` in ${functionName}(): called with '${argument}'`,
281+
];
282+
if (relevantStackLines.length > 0) {
283+
message.push(...relevantStackLines);
284+
}
285+
message.push(
286+
"",
287+
"[!] If this path is expected in your test environment, suppress it:",
288+
"",
289+
buildSuppressionSnippet("path-traversal", "ignore", originFrame, stack),
290+
);
291+
return message.join("\n");
292+
}

tests/bug-detectors/path-traversal.test.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ describe("Path Traversal", () => {
2727
const SAFE = "../safe_path/";
2828
const EVIL = "../evil_path/";
2929
const bugDetectorDirectory = path.join(__dirname, "path-traversal");
30+
const goalPath = path.join(bugDetectorDirectory, "../../jaz_zer");
3031

3132
beforeEach(async () => {
3233
fs.rmSync(SAFE, { recursive: true, force: true });
34+
fs.rmSync(goalPath, { recursive: true, force: true });
3335
await cleanCrashFilesIn(bugDetectorDirectory);
3436
});
3537

@@ -190,6 +192,63 @@ describe("Path Traversal", () => {
190192
.build();
191193
fuzzTest.execute();
192194
});
195+
196+
it("prints a copy-paste suppression snippet", () => {
197+
const fuzzTest = new FuzzTestBuilder()
198+
.runs(0)
199+
.sync(true)
200+
.fuzzEntryPoint("PathTraversalFsMkdirEvilSync")
201+
.dir(bugDetectorDirectory)
202+
.build();
203+
expect(() => {
204+
fuzzTest.execute();
205+
}).toThrow(FuzzingExitCode);
206+
expect(fuzzTest.stderr).toContain(
207+
'getBugDetectorConfiguration("path-traversal")',
208+
);
209+
expect(fuzzTest.stderr).toContain(".ignore({");
210+
expect(fuzzTest.stderr).toContain(
211+
"filePattern: /path-traversal[\\\\/]fuzz\\.js$/",
212+
);
213+
expect(fuzzTest.stderr).toContain("functionPattern: /^fn$/");
214+
});
215+
216+
it("suppresses findings when file and function both match", () => {
217+
const fuzzTest = new FuzzTestBuilder()
218+
.runs(0)
219+
.sync(true)
220+
.fuzzEntryPoint("PathTraversalFsMkdirEvilSync")
221+
.customHooks(["ignore-by-frame.config.js"])
222+
.dir(bugDetectorDirectory)
223+
.build();
224+
fuzzTest.execute();
225+
expect(fs.existsSync(goalPath)).toBeTruthy();
226+
});
227+
228+
it("suppresses findings when stack pattern matches", () => {
229+
const fuzzTest = new FuzzTestBuilder()
230+
.runs(0)
231+
.sync(true)
232+
.fuzzEntryPoint("PathTraversalJoinEvilSync")
233+
.customHooks(["ignore-by-stack.config.js"])
234+
.dir(bugDetectorDirectory)
235+
.build();
236+
fuzzTest.execute();
237+
});
238+
239+
it("still reports when ignore rule does not match", () => {
240+
const fuzzTest = new FuzzTestBuilder()
241+
.runs(0)
242+
.sync(true)
243+
.fuzzEntryPoint("PathTraversalJoinEvilSync")
244+
.customHooks(["ignore-no-match.config.js"])
245+
.dir(bugDetectorDirectory)
246+
.build();
247+
expect(() => {
248+
fuzzTest.execute();
249+
}).toThrow(FuzzingExitCode);
250+
expect(fuzzTest.stderr).toContain("Path Traversal");
251+
});
193252
});
194253

195254
describe("Path Traversal invalid input", () => {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2026 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
const {
18+
getBugDetectorConfiguration,
19+
} = require("../../../packages/bug-detectors");
20+
21+
getBugDetectorConfiguration("path-traversal")?.ignore({
22+
filePattern: /path-traversal[\\/]fuzz\.js$/,
23+
functionPattern: /^fn$/,
24+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2026 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
const {
18+
getBugDetectorConfiguration,
19+
} = require("../../../packages/bug-detectors");
20+
21+
getBugDetectorConfiguration("path-traversal")?.ignore({
22+
stackPattern: /Object\.join/,
23+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2026 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
const {
18+
getBugDetectorConfiguration,
19+
} = require("../../../packages/bug-detectors");
20+
21+
getBugDetectorConfiguration("path-traversal")?.ignore({
22+
filePattern: /never-match\.js$/,
23+
functionPattern: /^neverMatch$/,
24+
});

0 commit comments

Comments
 (0)