diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel
index 3711d89c0..57a72f963 100644
--- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel
+++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel
@@ -12,6 +12,10 @@ java_library(
java_library(
name = "file_path_traversal",
srcs = ["FilePathTraversal.java"],
+ visibility = [
+ "//sanitizers:__pkg__",
+ "//sanitizers/src/test/java/com/code_intelligence/jazzer/sanitizers:__pkg__",
+ ],
deps = ["//src/main/java/com/code_intelligence/jazzer/api:hooks"],
)
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java
index 507a65753..a2731d3b2 100644
--- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java
+++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java
@@ -24,59 +24,66 @@
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.logging.Level;
-import java.util.logging.Logger;
+import java.util.Optional;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
/**
* This tests for a file read or write of a specific file path whether relative or absolute.
*
*
This checks only for literal, absolute, normalized paths. It does not process symbolic links.
*
- *
The default target is {@link FilePathTraversal#DEFAULT_TARGET_STRING}
+ *
The default target is "../jazzer-traversal"."
*
- *
Users may customize a customize the target by setting the full path in the environment
- * variable {@link FilePathTraversal#FILE_PATH_TARGET_KEY}
+ *
Users may customize the target using the BugDetectors API, e.g. by {@code
+ * BugDetectors.setFilePathTraversalTarget(() -> Path.of("..", "jazzer-traversal"))}.
*
- *
This does not currently check for reading metadata from the target file.
+ *
TODO: This sanitizer does not currently check for reading metadata from the target file.
*/
public class FilePathTraversal {
- public static final String FILE_PATH_TARGET_KEY = "jazzer.file_path_traversal_target";
- public static final String DEFAULT_TARGET_STRING = "../jazzer-traversal";
+ public static final Path DEFAULT_TARGET = Paths.get("..", "jazzer-traversal");
- private static final Logger LOG = Logger.getLogger(FilePathTraversal.class.getName());
+ // Set via reflection by Jazzer's BugDetectors API.
+ public static final AtomicReference> target =
+ new AtomicReference<>(() -> DEFAULT_TARGET);
+ public static final AtomicReference> checkPath =
+ new AtomicReference<>((Path ignored) -> true);
- private static Path RELATIVE_TARGET;
- private static Path ABSOLUTE_TARGET;
- private static boolean IS_DISABLED = false;
- private static boolean IS_SET_UP = false;
+ // When guiding the fuzzer towards the target path, sometimes both the absolute and relative paths
+ // are valid. In this case, we toggle between them randomly.
+ // The random part is important because it is possible to set several targets in a fuzz test with
+ // try(target1...){
+ // ...
+ // try(target2...) {
+ // ...
+ // If we toggle in fix pattern, the fuzzer might guide towards the same blocks towards the same
+ // target.
+ // Randomizing the toggle counter sidesteps this issue.
+ private static final int MAX_TARGET_FOCUS_COUNT = 23;
+ private static boolean guideTowardsAbsoluteTargetPath = true;
+ private static int toggleCounter = 1;
- private static void setUp() {
- String customTarget = System.getProperty(FILE_PATH_TARGET_KEY);
- if (customTarget != null && !customTarget.isEmpty()) {
- LOG.log(Level.FINE, "custom target loaded: " + customTarget);
- setTargets(customTarget);
- } else {
- // check that this isn't being run at the root directory
- Path cwd = Paths.get(".").toAbsolutePath();
- if (cwd.getParent() == null) {
- LOG.warning(
- "Can't run from the root directory with the default target. "
- + "The FilePathTraversal sanitizer is disabled.");
- IS_DISABLED = true;
+ public static Optional toAbsolutePath(Path path, Path currentDir) {
+ try {
+ if (path.isAbsolute()) {
+ return Optional.of(path.normalize());
}
- setTargets(DEFAULT_TARGET_STRING);
+ return Optional.of(currentDir.resolve(path).normalize());
+ } catch (InvalidPathException e) {
+ return Optional.empty();
}
}
- private static void setTargets(String targetPath) {
- Path p = Paths.get(targetPath);
- Path pwd = Paths.get(".");
- if (p.isAbsolute()) {
- ABSOLUTE_TARGET = p.toAbsolutePath().normalize();
- RELATIVE_TARGET = pwd.toAbsolutePath().relativize(ABSOLUTE_TARGET).normalize();
- } else {
- ABSOLUTE_TARGET = pwd.resolve(p).toAbsolutePath().normalize();
- RELATIVE_TARGET = p.normalize();
+ public static Optional toRelativePath(Path path, Path currentDir) {
+ try {
+ if (path.isAbsolute()) {
+ return Optional.of(currentDir.relativize(path).normalize());
+ }
+ return Optional.of(path.normalize());
+ } catch (IllegalArgumentException e) {
+ return Optional.empty();
}
}
@@ -172,21 +179,11 @@ private static void setTargets(String targetPath) {
public static void pathFirstArgHook(
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
if (arguments.length > 0) {
- Object argObj = arguments[0];
- if (argObj instanceof Path) {
- checkPath((Path) argObj, hookId);
- }
+ detectAndGuidePathTraversal(arguments[0], hookId);
}
}
- /**
- * Checks to confirm that a path that is read from or written to is in an allowed directory.
- *
- * @param method
- * @param thisObject
- * @param arguments
- * @param hookId
- */
+ /** Checks to confirm that a path that is read from or written to is in an allowed directory. */
@MethodHook(
type = HookType.BEFORE,
targetClassName = "java.nio.file.Files",
@@ -202,14 +199,8 @@ public static void pathFirstArgHook(
public static void copyMismatchMvHook(
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
if (arguments.length > 1) {
- Object from = arguments[0];
- if (from instanceof Path) {
- checkPath((Path) from, hookId);
- }
- Object to = arguments[1];
- if (to instanceof Path) {
- checkPath((Path) to, hookId);
- }
+ detectAndGuidePathTraversal(arguments[0], hookId);
+ detectAndGuidePathTraversal(arguments[1], hookId);
}
}
@@ -220,7 +211,7 @@ public static void copyMismatchMvHook(
public static void fileReaderHook(
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
if (arguments.length > 0) {
- checkObj(arguments[0], hookId);
+ detectAndGuidePathTraversal(arguments[0], hookId);
}
}
@@ -231,7 +222,7 @@ public static void fileReaderHook(
public static void fileWriterHook(
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
if (arguments.length > 0) {
- checkObj(arguments[0], hookId);
+ detectAndGuidePathTraversal(arguments[0], hookId);
}
}
@@ -242,7 +233,7 @@ public static void fileWriterHook(
public static void fileInputStreamHook(
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
if (arguments.length > 0) {
- checkObj(arguments[0], hookId);
+ detectAndGuidePathTraversal(arguments[0], hookId);
}
}
@@ -253,7 +244,7 @@ public static void fileInputStreamHook(
public static void processFileOutputStartHook(
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
if (arguments.length > 0) {
- checkObj(arguments[0], hookId);
+ detectAndGuidePathTraversal(arguments[0], hookId);
}
}
@@ -264,7 +255,7 @@ public static void processFileOutputStartHook(
public static void scannerHook(
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
if (arguments.length > 0) {
- checkObj(arguments[0], hookId);
+ detectAndGuidePathTraversal(arguments[0], hookId);
}
}
@@ -275,82 +266,84 @@ public static void scannerHook(
public static void fileOutputStreamHook(
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
if (arguments.length > 0) {
- checkObj(arguments[0], hookId);
+ detectAndGuidePathTraversal(arguments[0], hookId);
}
}
- private static void checkObj(Object obj, int hookId) {
- if (obj instanceof String) {
- checkString((String) obj, hookId);
- } else if (obj instanceof Path) {
- checkPath((Path) obj, hookId);
- } else if (obj instanceof File) {
- checkFile((File) obj, hookId);
+ private static void detectAndGuidePathTraversal(Object obj, int hookId) {
+ if (obj == null) {
+ return;
}
- }
- private static void checkPath(Path p, int hookId) {
- check(p);
- Path normalized = p.normalize();
- if (p.isAbsolute()) {
- Jazzer.guideTowardsEquality(normalized.toString(), ABSOLUTE_TARGET.toString(), hookId);
- } else {
- Jazzer.guideTowardsEquality(normalized.toString(), RELATIVE_TARGET.toString(), hookId);
- }
- }
+ Path targetPath = target.get().get();
- private static void checkFile(File f, int hookId) {
- try {
- check(f.toPath());
- } catch (InvalidPathException e) {
- // TODO: give up -- for now
+ String query;
+ if (obj instanceof Path) {
+ query = ((Path) obj).normalize().toString();
+ } else if (obj instanceof File) {
+ try {
+ query = ((File) obj).toPath().normalize().toString();
+ } catch (InvalidPathException e) {
+ return;
+ }
+ } else if (obj instanceof String) {
+ try {
+ query = (String) obj;
+ } catch (InvalidPathException e) {
+ return;
+ }
+ } else { // not a path, file or string
return;
}
- Path normalized = f.toPath().normalize();
- if (normalized.isAbsolute()) {
- Jazzer.guideTowardsEquality(normalized.toString(), ABSOLUTE_TARGET.toString(), hookId);
- } else {
- Jazzer.guideTowardsEquality(normalized.toString(), RELATIVE_TARGET.toString(), hookId);
+
+ Predicate checkAllowed = checkPath.get();
+ boolean isPathAllowed = checkAllowed == null || checkAllowed.test(Paths.get(query).normalize());
+ if (!isPathAllowed) {
+ Jazzer.reportFindingFromHook(
+ new FuzzerSecurityIssueCritical(
+ "File path traversal: "
+ + query
+ + "\n Path is not allowed by the user-defined predicate."
+ + "\n Current path traversal fuzzing target: "
+ + targetPath));
}
- }
- private static void checkString(String s, int hookId) {
- try {
- check(Paths.get(s));
- } catch (InvalidPathException e) {
- checkFile(new File(s), hookId);
- // TODO -- give up for now
+ // Users can set the atomic function to return null to disable the fuzzer guidance.
+ if (targetPath == null) {
return;
}
- Path normalized = Paths.get(s);
- if (normalized.isAbsolute()) {
- Jazzer.guideTowardsEquality(s, ABSOLUTE_TARGET.toString(), hookId);
- } else {
- Jazzer.guideTowardsEquality(s, RELATIVE_TARGET.toString(), hookId);
- }
- }
+ targetPath = targetPath.normalize();
- private static void check(Path p) {
- // super lazy initialization -- race condition with unit test if this is set in a static block
- synchronized (LOG) {
- if (!IS_SET_UP) {
- setUp();
- IS_SET_UP = true;
- }
- }
- if (IS_DISABLED) {
+ Path currentDir = Paths.get("").toAbsolutePath();
+ Path absTarget = toAbsolutePath(targetPath, currentDir).orElse(null);
+ Path relTarget = toRelativePath(targetPath, currentDir).orElse(null);
+ if (absTarget == null && relTarget == null) {
return;
}
- // catch all exceptions that might be thrown by the sanitizer
- Path normalized;
- try {
- normalized = p.toAbsolutePath().normalize();
- } catch (Throwable e) {
- return;
+ if ((absTarget != null && absTarget.toString().equals(query))
+ || (relTarget != null && relTarget.toString().equals(query))) {
+ Jazzer.reportFindingFromHook(
+ new FuzzerSecurityIssueCritical(
+ "File path traversal: "
+ + query
+ + "\n Reached current path traversal fuzzing target: "
+ + targetPath));
}
- if (normalized.equals(ABSOLUTE_TARGET)) {
- Jazzer.reportFindingFromHook(new FuzzerSecurityIssueCritical("File path traversal: " + p));
+
+ if (absTarget != null && relTarget != null) {
+ if (guideTowardsAbsoluteTargetPath) {
+ Jazzer.guideTowardsContainment(query, absTarget.toString(), hookId);
+ } else {
+ Jazzer.guideTowardsContainment(query, relTarget.toString(), hookId);
+ }
+ if (--toggleCounter <= 0) {
+ guideTowardsAbsoluteTargetPath = !guideTowardsAbsoluteTargetPath;
+ toggleCounter = ThreadLocalRandom.current().nextInt(1, MAX_TARGET_FOCUS_COUNT + 1);
+ }
+ } else {
+ Jazzer.guideTowardsContainment(
+ query, (absTarget != null ? absTarget : relTarget).toString(), hookId);
}
}
}
diff --git a/sanitizers/src/test/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel b/sanitizers/src/test/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel
new file mode 100644
index 000000000..ff2617d88
--- /dev/null
+++ b/sanitizers/src/test/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel
@@ -0,0 +1,12 @@
+load("@contrib_rules_jvm//java:defs.bzl", "JUNIT5_DEPS", "java_junit5_test")
+
+java_junit5_test(
+ name = "FilePathTraversalTest",
+ srcs = ["FilePathTraversalTest.java"],
+ deps = JUNIT5_DEPS + [
+ "//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers:file_path_traversal",
+ "@maven//:com_google_truth_truth",
+ "@maven//:org_junit_jupiter_junit_jupiter_api",
+ "@maven//:org_junit_jupiter_junit_jupiter_params",
+ ],
+)
diff --git a/sanitizers/src/test/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversalTest.java b/sanitizers/src/test/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversalTest.java
new file mode 100644
index 000000000..79bdc7c23
--- /dev/null
+++ b/sanitizers/src/test/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversalTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2025 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.
+ */
+
+package com.code_intelligence.jazzer.sanitizers;
+
+import static com.code_intelligence.jazzer.sanitizers.FilePathTraversal.toAbsolutePath;
+import static com.code_intelligence.jazzer.sanitizers.FilePathTraversal.toRelativePath;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class FilePathTraversalTest {
+
+ static Stream pathsToRelative() {
+ // CWD, target, expectedRelative, expectedAbsolute
+ return Stream.of(
+ arguments(
+ Paths.get("/home/user1"),
+ Paths.get("test/A"),
+ Paths.get("test/A"),
+ Paths.get("/home/user1/test/A")),
+ arguments(
+ Paths.get("/home/user1"), Paths.get("../A"), Paths.get("../A"), Paths.get("/home/A")),
+ arguments(
+ Paths.get("/"), Paths.get("/test/me"), Paths.get("test/me"), Paths.get("/test/me")),
+ arguments(
+ Paths.get("/home/user1"),
+ Paths.get("/test/me"),
+ Paths.get("../../test/me"),
+ Paths.get("/test/me")),
+ arguments(
+ Paths.get("/home/user1"),
+ Paths.get("/home/user2/A/B/C"),
+ Paths.get("../user2/A/B/C"),
+ Paths.get("/home/user2/A/B/C")),
+ arguments(
+ Paths.get("/home/user1/Data"),
+ Paths.get("../A/B/C"),
+ Paths.get("../A/B/C"),
+ Paths.get("/home/user1/A/B/C")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("pathsToRelative")
+ @DisabledOnOs(OS.WINDOWS)
+ void toRelativeAndAbsolutePath_test(
+ Path cwd, Path target, Path expectedRelative, Path expectedAbsolute) {
+ Optional relative = toRelativePath(target, cwd);
+ if (expectedRelative == null) {
+ assertThat(relative).isEqualTo(Optional.empty());
+ } else {
+ assertThat(relative).isEqualTo(Optional.of(expectedRelative));
+ assertThat(expectedRelative.isAbsolute()).isFalse();
+ }
+ assertThat(toAbsolutePath(target, cwd)).isEqualTo(Optional.of(expectedAbsolute));
+ assertThat(expectedAbsolute.isAbsolute()).isTrue();
+ }
+
+ static Stream pathsToRelativeWin() {
+ // CWD, target, expectedRelative, expectedAbsolute
+ return Stream.of(
+ arguments(
+ Paths.get("C:\\home\\user1"),
+ Paths.get("test\\A"),
+ Paths.get("test\\A"),
+ Paths.get("C:\\home\\user1\\test\\A")),
+ arguments(
+ Paths.get("C:\\home\\user1"),
+ Paths.get("..\\A"),
+ Paths.get("..\\A"),
+ Paths.get("C:\\home\\A")),
+ arguments(
+ Paths.get("C:\\"),
+ Paths.get("C:\\test\\me"),
+ Paths.get("test\\me"),
+ Paths.get("C:\\test\\me")),
+ arguments(
+ Paths.get("C:\\home\\user1"),
+ Paths.get("C:\\test\\me"),
+ Paths.get("..\\..\\test\\me"),
+ Paths.get("C:\\test\\me")),
+ arguments(
+ Paths.get("C:\\home\\user1"),
+ Paths.get("C:\\home\\user2\\A\\B\\C"),
+ Paths.get("..\\user2\\A\\B\\C"),
+ Paths.get("C:\\home\\user2\\A\\B\\C")),
+ arguments(
+ Paths.get("C:\\home\\user1\\Data"),
+ Paths.get("..\\A\\B\\C"),
+ Paths.get("..\\A\\B\\C"),
+ Paths.get("C:\\home\\user1\\A\\B\\C")),
+ arguments(
+ Paths.get("C:\\home\\user1"),
+ Paths.get("D:\\A\\B\\C"),
+ null, // there is no relative path from CWD to D drive
+ Paths.get("D:\\A\\B\\C")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("pathsToRelativeWin")
+ @EnabledOnOs(OS.WINDOWS)
+ void toRelativePath_test_windows(
+ Path cwd, Path target, Path expectedRelative, Path expectedAbsolute) {
+
+ Optional relative = toRelativePath(target, cwd);
+ if (expectedRelative == null) {
+ assertThat(relative).isEqualTo(Optional.empty());
+ } else {
+ assertThat(relative).isEqualTo(Optional.of(expectedRelative));
+ assertThat(expectedRelative.isAbsolute()).isFalse();
+ }
+ assertThat(toAbsolutePath(target, cwd)).isEqualTo(Optional.of(expectedAbsolute));
+ assertThat(expectedAbsolute.isAbsolute()).isTrue();
+ }
+}
diff --git a/sanitizers/src/test/java/com/example/AbsoluteFilePathTraversal.java b/sanitizers/src/test/java/com/example/AbsoluteFilePathTraversal.java
index f6a7648f5..7aa033b49 100644
--- a/sanitizers/src/test/java/com/example/AbsoluteFilePathTraversal.java
+++ b/sanitizers/src/test/java/com/example/AbsoluteFilePathTraversal.java
@@ -16,7 +16,7 @@
package com.example;
-import com.code_intelligence.jazzer.mutation.annotation.DoubleInRange;
+import com.code_intelligence.jazzer.api.BugDetectors;
import com.code_intelligence.jazzer.mutation.annotation.NotNull;
import com.code_intelligence.jazzer.mutation.annotation.WithUtf8Length;
import java.io.BufferedReader;
@@ -29,24 +29,18 @@
public class AbsoluteFilePathTraversal {
static {
- System.setProperty("jazzer.file_path_traversal_target", "/custom/path/jazzer-traversal");
+ BugDetectors.setFilePathTraversalTarget(
+ () -> Paths.get("/", "custom", "path", "jazzer-traversal"));
}
- public static void fuzzerTestOneInput(
- @WithUtf8Length(max = 100) @NotNull String pathFromFuzzer,
- @NotNull @DoubleInRange(min = 0.0, max = 1.0) Double fixedPathProbability) {
- // Slow down the fuzzer a bit, otherwise it finds file path traversal way too quickly!
- String path = fixedPathProbability < 0.95 ? "/a/b/c/fixed-path" : pathFromFuzzer;
-
+ public static void fuzzerTestOneInput(@WithUtf8Length(max = 100) @NotNull String path) {
try {
Path p = Paths.get(path);
try (BufferedReader r = Files.newBufferedReader(p, StandardCharsets.UTF_8)) {
r.read();
} catch (IOException ignored) {
- // swallow
}
} catch (InvalidPathException ignored) {
- // swallow
}
}
}
diff --git a/sanitizers/src/test/java/com/example/BUILD.bazel b/sanitizers/src/test/java/com/example/BUILD.bazel
index d7f4c9d6e..0405d9ffe 100644
--- a/sanitizers/src/test/java/com/example/BUILD.bazel
+++ b/sanitizers/src/test/java/com/example/BUILD.bazel
@@ -67,7 +67,6 @@ java_fuzz_target_test(
"com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical",
],
target_class = "com.example.AbsoluteFilePathTraversal",
- #not clear why reproducer doesn't work TODO -- fix this
verify_crash_reproducer = False,
deps = [
"//src/main/java/com/code_intelligence/jazzer/mutation/annotation",
@@ -83,13 +82,77 @@ java_fuzz_target_test(
"com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical",
],
target_class = "com.example.FilePathTraversal",
- #not clear why reproducer doesn't work TODO -- fix this
verify_crash_reproducer = False,
deps = [
"//src/main/java/com/code_intelligence/jazzer/mutation/annotation",
],
)
+[java_fuzz_target_test(
+ name = "FilePathTraversalPass_" + method,
+ srcs = [
+ "FilePathTraversalPass.java",
+ ],
+ env = {
+ "JAZZER_FUZZ": "1",
+ },
+ fuzzer_args = [
+ "-runs=0",
+ ],
+ target_class = "com.example.FilePathTraversalPass",
+ target_method = method,
+ verify_crash_reproducer = False,
+ runtime_deps = [
+ "@maven//:org_junit_jupiter_junit_jupiter_engine",
+ ],
+ deps = [
+ "//deploy:jazzer-junit",
+ "@maven//:org_junit_jupiter_junit_jupiter_api",
+ ],
+) for method in [
+ "beforeEachWorks",
+ "overwritingBeforeEachWorks",
+ "allow",
+ "targetMissed",
+ "onion",
+]]
+
+[java_fuzz_target_test(
+ name = "FilePathTraversalCrash_" + method,
+ srcs = [
+ "FilePathTraversalCrash.java",
+ ],
+ allowed_findings = [
+ "com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical",
+ ],
+ env = {
+ "JAZZER_FUZZ": "1",
+ },
+ expect_number_of_findings = 1,
+ fuzzer_args = [
+ "-runs=0",
+ ],
+ target_class = "com.example.FilePathTraversalCrash",
+ target_method = method,
+ verify_crash_reproducer = False,
+ runtime_deps = [
+ "@maven//:org_junit_jupiter_junit_jupiter_engine",
+ ],
+ deps = [
+ "//deploy:jazzer-junit",
+ "@maven//:org_junit_jupiter_junit_jupiter_api",
+ ],
+) for method in [
+ "beforeEachWorks",
+ "overwritingBeforeEachWorks",
+ "crashWhenAllowIsFalse",
+ "crashWhenDefaultTarget",
+ "onionTarget",
+ "cascadedTarget",
+ "absoluteToRelative",
+ "relativeToAbsolute",
+]]
+
java_fuzz_target_test(
name = "OsCommandInjectionProcessBuilder",
srcs = [
diff --git a/sanitizers/src/test/java/com/example/FilePathTraversal.java b/sanitizers/src/test/java/com/example/FilePathTraversal.java
index 28121733f..f17248931 100644
--- a/sanitizers/src/test/java/com/example/FilePathTraversal.java
+++ b/sanitizers/src/test/java/com/example/FilePathTraversal.java
@@ -16,7 +16,6 @@
package com.example;
-import com.code_intelligence.jazzer.mutation.annotation.DoubleInRange;
import com.code_intelligence.jazzer.mutation.annotation.NotNull;
import com.code_intelligence.jazzer.mutation.annotation.WithUtf8Length;
import java.io.BufferedReader;
@@ -29,20 +28,14 @@
public class FilePathTraversal {
- public static void fuzzerTestOneInput(
- @WithUtf8Length(max = 100) @NotNull String pathFromFuzzer,
- @NotNull @DoubleInRange(min = 0.0, max = 1.0) Double fixedPathProbability) {
- // Slow down the fuzzer a bit, otherwise it finds file path traversal way too quickly!
- String path = fixedPathProbability < 0.95 ? "/a/b/c/fixed-path" : pathFromFuzzer;
+ public static void fuzzerTestOneInput(@WithUtf8Length(max = 100) @NotNull String path) {
try {
Path p = Paths.get(path);
try (BufferedReader r = Files.newBufferedReader(p, StandardCharsets.UTF_8)) {
r.read();
} catch (IOException ignored) {
- // swallow
}
} catch (InvalidPathException ignored) {
- // swallow
}
}
}
diff --git a/sanitizers/src/test/java/com/example/FilePathTraversalCrash.java b/sanitizers/src/test/java/com/example/FilePathTraversalCrash.java
new file mode 100644
index 000000000..df503bb91
--- /dev/null
+++ b/sanitizers/src/test/java/com/example/FilePathTraversalCrash.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2025 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.
+ */
+
+package com.example;
+
+import com.code_intelligence.jazzer.api.BugDetectors;
+import com.code_intelligence.jazzer.api.SilentCloseable;
+import com.code_intelligence.jazzer.junit.FuzzTest;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.jupiter.api.BeforeEach;
+
+public class FilePathTraversalCrash {
+ @BeforeEach
+ public void setUp() {
+ BugDetectors.setFilePathTraversalTarget(() -> Paths.get("..", "..", "hello"));
+ }
+
+ @FuzzTest
+ void beforeEachWorks(boolean ignore) throws Exception {
+ tryPathTraversal("..", "..", "hello");
+ }
+
+ @FuzzTest
+ void overwritingBeforeEachWorks(boolean ignore) {
+ try (SilentCloseable unused = setTarget("..", "..", "jazzer-hey")) {
+ tryPathTraversal("..", "..", "jazzer-hey");
+ }
+ }
+
+ @FuzzTest
+ void crashWhenAllowIsFalse(boolean ignore) {
+ try (SilentCloseable unused = BugDetectors.setFilePathTraversalAllowPath((Path p) -> false)) {
+ tryPathTraversal("any-path-is-bad");
+ }
+ }
+
+ @FuzzTest
+ void crashWhenDefaultTarget(boolean ignore) {
+ try (SilentCloseable unused = BugDetectors.setFilePathTraversalAllowPath((Path p) -> true)) {
+ tryPathTraversal("..", "..", "hello");
+ }
+ }
+
+ @FuzzTest
+ void onionTarget(boolean ignore) {
+ try (SilentCloseable unused = setTarget("..", "..", "jazzer-hey")) {
+ try (SilentCloseable unused1 = setTarget("..", "..", "jazzer-hey1")) {
+ try (SilentCloseable unused2 = setTarget("..", "..", "jazzer-hey2")) {
+ tryPathTraversal("..", "..", "jazzer-hey2");
+ }
+ }
+ }
+ }
+
+ @FuzzTest
+ void cascadedTarget(boolean ignore) {
+ try (SilentCloseable ignore1 = setTarget("..", "..", "jazzer-hey")) {
+ // ignore
+ }
+ try (SilentCloseable ignore2 = setTarget("..", "..", "jazzer-hey1")) {
+ // ignore
+ }
+ try (SilentCloseable ignore3 = setTarget("..", "..", "jazzer-hey2")) {
+ tryPathTraversal("..", "..", "jazzer-hey2");
+ }
+ }
+
+ @FuzzTest
+ void absoluteToRelative(boolean ignore) {
+ final Path cwd = Paths.get("").toAbsolutePath();
+ final Path target = Paths.get("test", "A");
+ // set absolute target
+ try (SilentCloseable ignore1 = setTarget(cwd.resolve(target))) {
+ // try to read relative path
+ tryPathTraversal(target);
+ }
+ }
+
+ @FuzzTest
+ void relativeToAbsolute(boolean ignore) {
+ final Path cwd = Paths.get("").toAbsolutePath();
+ final Path target = Paths.get("test", "A");
+ // set relative target
+ try (SilentCloseable ignore1 = setTarget(target)) {
+ // try to read absolute path
+ tryPathTraversal(cwd.resolve(target));
+ }
+ }
+
+ private static SilentCloseable setTarget(String first, String... rest) {
+ return setTarget(Paths.get(first, rest));
+ }
+
+ private static SilentCloseable setTarget(Path p) {
+ return BugDetectors.setFilePathTraversalTarget(() -> p);
+ }
+
+ private static void tryPathTraversal(String part1, String... rest) {
+ Path path = Paths.get(part1, rest);
+ try (FileInputStream fis = new FileInputStream(path.toString())) {
+ fis.read();
+ } catch (NullPointerException | IOException ignored) {
+ }
+ }
+
+ private static void tryPathTraversal(Path path) {
+ try (FileInputStream fis = new FileInputStream(path.toString())) {
+ fis.read();
+ } catch (NullPointerException | IOException ignored) {
+ }
+ }
+}
diff --git a/sanitizers/src/test/java/com/example/FilePathTraversalPass.java b/sanitizers/src/test/java/com/example/FilePathTraversalPass.java
new file mode 100644
index 000000000..2776281c9
--- /dev/null
+++ b/sanitizers/src/test/java/com/example/FilePathTraversalPass.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2025 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.
+ */
+
+package com.example;
+
+import com.code_intelligence.jazzer.api.BugDetectors;
+import com.code_intelligence.jazzer.api.SilentCloseable;
+import com.code_intelligence.jazzer.junit.FuzzTest;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.jupiter.api.BeforeEach;
+
+public class FilePathTraversalPass {
+ @BeforeEach
+ public void setUp() {
+ setTarget(Paths.get("..", "..", "hello"));
+ }
+
+ @FuzzTest
+ void beforeEachWorks(boolean ignore) {
+ tryPathTraversal("test");
+ }
+
+ @FuzzTest
+ void overwritingBeforeEachWorks(boolean ignore) {
+ try (SilentCloseable ignore1 = setTarget(Paths.get("..", "..", "jazzer-hey"))) {
+ tryPathTraversal("..", "..", "hello");
+ }
+ }
+
+ @FuzzTest
+ void allow(boolean ignore) {
+ try (SilentCloseable ignore1 =
+ BugDetectors.setFilePathTraversalAllowPath((Path p) -> p.toString().contains("secret"))) {
+ tryPathTraversal("my-secret-file");
+ }
+ }
+
+ @FuzzTest
+ void targetMissed(boolean ignore) {
+ try (SilentCloseable ignore1 =
+ BugDetectors.setFilePathTraversalAllowPath((Path ignoredAgain) -> true)) {
+ tryPathTraversal("some-path");
+ }
+ }
+
+ @FuzzTest
+ void onion(boolean ignore) {
+ final Path jazzerHello = Paths.get("..", "..", "hello");
+ final Path jazzerTest = Paths.get("test");
+ final Path jazzerHey = Paths.get("..", "..", "jazzer-hey");
+ final Path jazzerHey1 = Paths.get("..", "..", "jazzer-hey1");
+
+ try (SilentCloseable ignored = setTarget(jazzerHey)) {
+ tryPathTraversal(jazzerHello);
+ try (SilentCloseable ignored1 = setTarget(jazzerTest)) {
+ tryPathTraversal(jazzerHey);
+ try (SilentCloseable ignored2 = setTarget(jazzerHey1)) {
+ tryPathTraversal(jazzerTest);
+ }
+ tryPathTraversal(jazzerHey);
+ tryPathTraversal(jazzerHey1);
+ }
+ tryPathTraversal(jazzerTest);
+ tryPathTraversal(jazzerHey1);
+ tryPathTraversal(jazzerHello);
+ }
+ }
+
+ private static SilentCloseable setTarget(String first, String... rest) {
+ return setTarget(Paths.get(first, rest));
+ }
+
+ private static SilentCloseable setTarget(Path p) {
+ return BugDetectors.setFilePathTraversalTarget(() -> p);
+ }
+
+ private static void tryPathTraversal(String first, String... rest) {
+ Path path = Paths.get(first, rest);
+ try (FileInputStream fis = new FileInputStream(path.toString())) {
+ fis.read();
+ } catch (NullPointerException | IOException ignored) {
+ }
+ }
+
+ private static void tryPathTraversal(Path path) {
+ try (FileInputStream fis = new FileInputStream(path.toString())) {
+ fis.read();
+ } catch (NullPointerException | IOException ignored) {
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/BugDetectors.java b/src/main/java/com/code_intelligence/jazzer/api/BugDetectors.java
index 3784a7c2f..7d817ea6e 100644
--- a/src/main/java/com/code_intelligence/jazzer/api/BugDetectors.java
+++ b/src/main/java/com/code_intelligence/jazzer/api/BugDetectors.java
@@ -16,13 +16,18 @@
package com.code_intelligence.jazzer.api;
+import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiPredicate;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
/** Provides static functions that configure the behavior of bug detectors provided by Jazzer. */
public final class BugDetectors {
private static final AtomicReference> currentPolicy =
- getConnectionPermittedReference();
+ getSanitizerVariable(
+ "com.code_intelligence.jazzer.sanitizers.ServerSideRequestForgery",
+ "connectionPermitted");
/**
* Allows all network connections.
@@ -76,28 +81,74 @@ public static SilentCloseable allowNetworkConnections() {
*/
public static SilentCloseable allowNetworkConnections(
BiPredicate connectionPermitted) {
- if (connectionPermitted == null) {
- throw new IllegalArgumentException("connectionPermitted must not be null");
- }
- if (currentPolicy == null) {
- throw new IllegalStateException("Failed to set network connection policy");
- }
- BiPredicate previousPolicy = currentPolicy.getAndSet(connectionPermitted);
- return () -> {
- if (!currentPolicy.compareAndSet(connectionPermitted, previousPolicy)) {
- throw new IllegalStateException(
- "Failed to reset network connection policy - using try-with-resources is highly"
- + " recommended");
- }
- };
+ return setSanitizerVariable(connectionPermitted, currentPolicy);
+ }
+
+ // File path traversal sanitizer control
+ private static final AtomicReference> currentPathTraversalTarget =
+ getSanitizerVariable("com.code_intelligence.jazzer.sanitizers.FilePathTraversal", "target");
+
+ /**
+ * Sets the target for file path traversal sanitization. If the target is reached, a finding is
+ * thrown. The target is also used to guide the fuzzer to intentionally trigger file path
+ * traversal.
+ *
+ * By default, the file path traversal target is set to return {@code "../jazzer-traversal"}.
+ *
+ *
Setting the path traversal target supplier to return {@code null } will disable the
+ * guidance.
+ *
+ *
By wrapping the call into a try-with-resources statement, the target can be configured to
+ * apply to individual parts of the fuzz test only:
+ *
+ *
{@code
+ * try (SilentCloseable unused = BugDetectors.setFilePathTraversalTarget(() -> Paths.get("/root"))) {
+ * // Perform operations that require file path traversal sanitization
+ * }
+ * }
+ *
+ * @param pathTraversalTarget a supplier that provides the target directory for file path
+ * traversal sanitization
+ * @return a {@link SilentCloseable} that restores the previously set target when closed
+ */
+ public static SilentCloseable setFilePathTraversalTarget(Supplier pathTraversalTarget) {
+ return setSanitizerVariable(pathTraversalTarget, currentPathTraversalTarget);
+ }
+
+ private static final AtomicReference> currentCheckPath =
+ getSanitizerVariable(
+ "com.code_intelligence.jazzer.sanitizers.FilePathTraversal", "checkPath");
+
+ /**
+ * Sets the predicate that determines if a file path is allowed to be accessed. Paths that are not
+ * allowed will trigger a file path traversal finding. If you use this method, don't forget to set
+ * the fuzzing target with {@code setFilePathTraversalTarget} that aligns with this predicate,
+ * because both {@code target} and {@code checkPath} can trigger a finding independently.
+ *
+ * By default, all file paths are allowed. Setting the predicate to {@code false} will trigger
+ * a file path traversal finding for any file path access.
+ *
+ *
By wrapping the call into a try-with-resources statement, the predicate can be configured to
+ * apply to individual parts of the fuzz test only:
+ *
+ *
{@code
+ * try (SilentCloseable unused = BugDetectors.setFilePathTraversalAllowPath(
+ * (Path p) -> p.toString().contains("secret"))) {
+ * // Perform operations that require file path traversal sanitization
+ * }
+ * }
+ *
+ * @param checkPath a predicate that evaluates to {@code true} if the file path is allowed
+ * @return a {@link SilentCloseable} that restores the previously set predicate when closed
+ */
+ public static SilentCloseable setFilePathTraversalAllowPath(Predicate checkPath) {
+ return setSanitizerVariable(checkPath, currentCheckPath);
}
- private static AtomicReference> getConnectionPermittedReference() {
+ private static AtomicReference getSanitizerVariable(
+ String sanitizerClassName, String fieldName) {
try {
- Class> ssrfSanitizer =
- Class.forName("com.code_intelligence.jazzer.sanitizers.ServerSideRequestForgery");
- return (AtomicReference>)
- ssrfSanitizer.getField("connectionPermitted").get(null);
+ return (AtomicReference) Class.forName(sanitizerClassName).getField(fieldName).get(null);
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
System.err.println("WARN: ");
e.printStackTrace();
@@ -105,5 +156,23 @@ private static AtomicReference> getConnectionPermit
}
}
+ private static SilentCloseable setSanitizerVariable(
+ T newValue, AtomicReference currentValue) {
+ if (newValue == null) {
+ throw new IllegalArgumentException("sanitizer variable must not be null");
+ }
+ if (currentValue == null) {
+ throw new IllegalStateException("Failed to set sanitizer variable");
+ }
+ T previousValue = currentValue.getAndSet(newValue);
+ return () -> {
+ if (!currentValue.compareAndSet(newValue, previousValue)) {
+ throw new IllegalStateException(
+ "Failed to reset sanitizer variable - using try-with-resources is highly"
+ + " recommended");
+ }
+ };
+ }
+
private BugDetectors() {}
}