Skip to content

Commit adf5ab3

Browse files
committed
feat(path-traversal): add stack suppressions
Let users silence expected traversal callsites with the same shown-stack rules used by code-injection findings.
1 parent 03c5ad0 commit adf5ab3

5 files changed

Lines changed: 200 additions & 2 deletions

File tree

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

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

23+
import { bugDetectorConfigurations } from "../configuration";
24+
import {
25+
buildGenericSuppressionSnippet,
26+
captureStack,
27+
getUserFacingStackLines,
28+
IgnoreList,
29+
type IgnoreRule,
30+
} from "../shared/finding-suppression";
31+
2332
/**
2433
* Importing this file adds "before-hooks" for all functions in the built-in `fs`, `fs/promises`, and `path` module and guides
2534
* the fuzzer towards the uniquely chosen `goal` string `"../../jaz_zer"`. If the goal is found in the first argument
2635
* of any hooked function, a `Finding` is reported.
2736
*/
2837
const goal = "../../jaz_zer";
38+
39+
export type { IgnoreRule } from "../shared/finding-suppression";
40+
41+
/**
42+
* Configuration for the Path Traversal bug detector.
43+
* Controls suppression of matched path traversal findings.
44+
*/
45+
export interface PathTraversalConfig {
46+
/**
47+
* Suppresses findings that match the provided rule.
48+
* Use this to silence known-safe callsites in your test environment.
49+
*/
50+
ignore(rule: IgnoreRule): this;
51+
}
52+
53+
class PathTraversalConfigImpl implements PathTraversalConfig {
54+
private readonly _ignoredRules = new IgnoreList();
55+
56+
ignore(rule: IgnoreRule): this {
57+
this._ignoredRules.add(rule);
58+
return this;
59+
}
60+
61+
shouldReport(stack: string): boolean {
62+
return !this._ignoredRules.matches(stack);
63+
}
64+
}
65+
66+
const config = new PathTraversalConfigImpl();
67+
bugDetectorConfigurations.set("path-traversal", config);
68+
2969
const modulesToHook = [
3070
{
3171
moduleName: "fs",
@@ -208,11 +248,38 @@ function detectFindingAndGuideFuzzing(
208248
) {
209249
const argument = input.toString();
210250
if (argument.includes(goal)) {
251+
const stack = captureStack();
252+
if (!config.shouldReport(stack)) {
253+
return;
254+
}
211255
reportAndThrowFinding(
212-
"Path Traversal\n" +
213-
` in ${functionName}(): called with '${argument}'`,
256+
buildFindingMessage(functionName, argument, stack),
257+
false,
214258
);
215259
}
216260
guideTowardsContainment(argument, goal, hookId);
217261
}
218262
}
263+
264+
function buildFindingMessage(
265+
functionName: string,
266+
argument: string,
267+
stack: string,
268+
): string {
269+
const relevantStackLines = getUserFacingStackLines(stack);
270+
const message = [
271+
"Path Traversal",
272+
` in ${functionName}(): called with '${argument}'`,
273+
];
274+
if (relevantStackLines.length > 0) {
275+
message.push(...relevantStackLines);
276+
}
277+
message.push(
278+
"",
279+
"[!] If this callsite is expected in your test environment, suppress it:",
280+
" Example only: copy/paste it and adapt `stackPattern` to your needs.",
281+
"",
282+
buildGenericSuppressionSnippet("path-traversal", "ignore"),
283+
);
284+
return message.join("\n");
285+
}

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

Lines changed: 62 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,66 @@ describe("Path Traversal", () => {
190192
.build();
191193
fuzzTest.execute();
192194
});
195+
196+
it("prints a generic suppression example", () => {
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(
210+
"Example only: copy/paste it and adapt `stackPattern` to your needs.",
211+
);
212+
expect(fuzzTest.stderr).toContain(
213+
"// Example only: adapt `stackPattern` to the shown stack above.",
214+
);
215+
expect(fuzzTest.stderr).toContain("?.ignore({");
216+
expect(fuzzTest.stderr).toContain('stackPattern: "test.js:10"');
217+
});
218+
219+
it("suppresses findings when a stack pattern matches the fs callsite", () => {
220+
const fuzzTest = new FuzzTestBuilder()
221+
.runs(0)
222+
.sync(true)
223+
.fuzzEntryPoint("PathTraversalFsMkdirEvilSync")
224+
.customHooks(["ignore-fs-stack.config.js"])
225+
.dir(bugDetectorDirectory)
226+
.build();
227+
fuzzTest.execute();
228+
expect(fs.existsSync(goalPath)).toBeTruthy();
229+
});
230+
231+
it("suppresses findings when stack pattern matches", () => {
232+
const fuzzTest = new FuzzTestBuilder()
233+
.runs(0)
234+
.sync(true)
235+
.fuzzEntryPoint("PathTraversalJoinEvilSync")
236+
.customHooks(["ignore-by-stack.config.js"])
237+
.dir(bugDetectorDirectory)
238+
.build();
239+
fuzzTest.execute();
240+
});
241+
242+
it("still reports when ignore rule does not match", () => {
243+
const fuzzTest = new FuzzTestBuilder()
244+
.runs(0)
245+
.sync(true)
246+
.fuzzEntryPoint("PathTraversalJoinEvilSync")
247+
.customHooks(["ignore-no-match.config.js"])
248+
.dir(bugDetectorDirectory)
249+
.build();
250+
expect(() => {
251+
fuzzTest.execute();
252+
}).toThrow(FuzzingExitCode);
253+
expect(fuzzTest.stderr).toContain("Path Traversal");
254+
});
193255
});
194256

195257
describe("Path Traversal invalid input", () => {
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: 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: "at fn (",
23+
});
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: "never-match",
23+
});

0 commit comments

Comments
 (0)