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() {} }