From 9c37025d76a2715cf0bd022ab13929dbc847c052 Mon Sep 17 00:00:00 2001 From: tallison Date: Tue, 17 Dec 2024 16:48:58 -0500 Subject: [PATCH 1/5] initial path traversal sanitizer --- sanitizers/sanitizers.bzl | 1 + .../jazzer/sanitizers/BUILD.bazel | 7 + .../jazzer/sanitizers/FilePathTraversal.java | 352 ++++++++++++++++++ .../src/test/java/com/example/BUILD.bazel | 13 + .../java/com/example/FilePathTraversal.java | 49 +++ 5 files changed, 422 insertions(+) create mode 100644 sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java create mode 100644 sanitizers/src/test/java/com/example/FilePathTraversal.java diff --git a/sanitizers/sanitizers.bzl b/sanitizers/sanitizers.bzl index 0ac523fe2..ffa30114a 100644 --- a/sanitizers/sanitizers.bzl +++ b/sanitizers/sanitizers.bzl @@ -21,6 +21,7 @@ _sanitizer_class_names = [ "ClojureLangHooks", "Deserialization", "ExpressionLanguageInjection", + "FilePathTraversal", "LdapInjection", "NamingContextLookup", "OsCommandInjection", 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 2498c8df1..60d2e02e9 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 @@ -9,6 +9,12 @@ java_library( deps = ["//src/main/java/com/code_intelligence/jazzer/api:hooks"], ) +java_library( + name = "file_path_traversal", + srcs = ["FilePathTraversal.java"], + deps = ["//src/main/java/com/code_intelligence/jazzer/api:hooks"], +) + java_library( name = "regex_roadblocks", srcs = ["RegexRoadblocks.java"], @@ -58,6 +64,7 @@ kt_jvm_library( visibility = ["//sanitizers:__pkg__"], runtime_deps = [ ":clojure_lang_hooks", + ":file_path_traversal", ":regex_roadblocks", ":script_engine_injection", ":server_side_request_forgery", 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 new file mode 100644 index 000000000..9ec5ea69f --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/FilePathTraversal.java @@ -0,0 +1,352 @@ +package com.code_intelligence.jazzer.sanitizers; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical; +import com.code_intelligence.jazzer.api.HookType; +import com.code_intelligence.jazzer.api.Jazzer; +import com.code_intelligence.jazzer.api.MethodHook; + +import java.io.IOException; +import java.io.File; +import java.lang.invoke.MethodHandle; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * This tests for a file read or write of a specific file name AND + * whether that file is in an allowed directory or a descendant. + *

+ * This checks only for literal, absolute, normalized paths. It does not process symbolic links. + *

+ * This sanitizer will only trigger if {@link FilePathTraversal#ALLOWED_DIRS_KEY} is + * set as an environment variable. If that is not set, this sanitizer is a no-op. + *

+ * This does not check for reading metadata from files outside of the allowed directories. + */ +public class FilePathTraversal { + public static final String FILE_NAME_ENV_KEY = "JAZZER_FILE_SYSTEM_TRAVERSAL_FILE_NAME"; + public static final String ALLOWED_DIRS_KEY = "jazzer.fs_allowed_dirs"; + public static final String DEFAULT_SENTINEL = "jazzer-traversal"; + public static final String SENTINEL = + (System.getenv(FILE_NAME_ENV_KEY) == null || + System.getenv(FILE_NAME_ENV_KEY).trim().length() == 0) ? + DEFAULT_SENTINEL : System.getenv(FILE_NAME_ENV_KEY); + + //intentionally skipping createLink and createSymbolicLink + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createDirectory" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createDirectories" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createFile" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createTempDirectory" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createTempFile" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "delete" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "deleteIfExists" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "lines" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newByteChannel" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newBufferedReader" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newBufferedWriter" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readString" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newBufferedReader" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readAllBytes" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readAllLines" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readSymbolicLink" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "write" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "writeString" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newInputStream" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newOutputStream" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.probeContentType", + targetMethod = "open" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.channels.FileChannel", + targetMethod = "open" + ) + 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); + } + } + } + + /** + * 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 + */ + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "copy" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "mismatch" + ) + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "move" + ) + 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); + } + Object to = arguments[1]; + if (to instanceof Path) { + checkPath((Path) to); + } + } + } + + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileReader", + targetMethod = "" + ) + public static void fileReaderHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + + Object argObj = arguments[0]; + if (argObj instanceof String) { + checkPath((String)argObj); + } else if (argObj instanceof File) { + checkPath((File)argObj); + } + } + } + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileWriter", + targetMethod = "" + ) + public static void fileWriterHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + + Object argObj = arguments[0]; + if (argObj instanceof String) { + checkPath((String)argObj); + } else if (argObj instanceof File) { + checkPath((File)argObj); + } + } + } + + + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileInputStream", + targetMethod = "" + ) + public static void fileInputStreamHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + + Object argObj = arguments[0]; + if (argObj instanceof String) { + checkPath((String)argObj); + } else if (argObj instanceof File) { + checkPath((File)argObj); + } + } + } + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileOutputStream", + targetMethod = "" + ) + public static void processFileOutputStartHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + Object argObj = arguments[0]; + if (argObj instanceof File) { + if (argObj instanceof String) { + checkPath((String)argObj); + } else if (argObj instanceof File) { + checkPath((File)argObj); + } + } + } + } + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.util.Scanner", + targetMethod = "" + ) + public static void scannerHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + + Object argObj = arguments[0]; + if (argObj instanceof String) { + checkPath((String)argObj); + } else if (argObj instanceof Path) { + checkPath((Path)argObj); + } else if (argObj instanceof File) { + checkPath((File)argObj); + } + } + } + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileOutputStream", + targetMethod = "" + ) + public static void fileOutputStreamHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + + Object argObj = arguments[0]; + if (argObj instanceof File) { + checkPath((File)argObj); + } else if (argObj instanceof String) { + checkPath((String)argObj); + } + } + } + + private static void checkPath(File f) { + try { + checkPath(f.toPath()); + } catch (InvalidPathException e) { + //TODO: give up -- for now + } + } + + private static void checkPath(String s) { + try { + checkPath(Paths.get(s)); + } catch (InvalidPathException e) { + checkPath(new File(s)); + } + } + + private static void checkPath(Path p) { + if (p.getFileName().toString().equals(SENTINEL) && ! isAllowed(p)) { + Jazzer.reportFindingFromHook( + new FuzzerSecurityIssueCritical("File path traversal: " + p)); + } + } + + private static boolean isAllowed(Path candidate) { + String allowedDirString = System.getProperty(ALLOWED_DIRS_KEY); + + if (allowedDirString == null || allowedDirString.trim().length() == 0) { + return true; + } + + Path candidateNormalized = candidate.toAbsolutePath().normalize(); + for (String pString : allowedDirString.split(",")) { + Path allowedNormalized = Paths.get(pString).toAbsolutePath().normalize(); + if (candidateNormalized.startsWith(allowedNormalized) && + ! candidateNormalized.equals(allowedNormalized)) { + return true; + } + } + return false; + } + +} + diff --git a/sanitizers/src/test/java/com/example/BUILD.bazel b/sanitizers/src/test/java/com/example/BUILD.bazel index 2233c5350..189fa0172 100644 --- a/sanitizers/src/test/java/com/example/BUILD.bazel +++ b/sanitizers/src/test/java/com/example/BUILD.bazel @@ -58,6 +58,19 @@ java_fuzz_target_test( ], ) +java_fuzz_target_test( + name = "FilePathTraversal", + srcs = [ + "FilePathTraversal.java", + ], + allowed_findings = [ + "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, +) + 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 new file mode 100644 index 000000000..1e21a47d1 --- /dev/null +++ b/sanitizers/src/test/java/com/example/FilePathTraversal.java @@ -0,0 +1,49 @@ +// +// 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 java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; + +public class FilePathTraversal { + + static { + System.setProperty("jazzer.fs_allowed_dirs", + System.getProperty("java.io.tmpdir") + "," + + "/a/b/c/allowed"); + } + + public static void fuzzerTestOneInput(FuzzedDataProvider fuzzedDataProvider) { + String data = fuzzedDataProvider.consumeString(100); + String path = fuzzedDataProvider.consumeProbabilityDouble() < 0.1 ? + "/a/b/d/e/jazzer-traversal" : data; + 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 + } + } +} From e3e2a16bbdfaf239c0e8c5d20dce8d1e9211eaa2 Mon Sep 17 00:00:00 2001 From: tallison Date: Wed, 18 Dec 2024 08:36:05 -0500 Subject: [PATCH 2/5] add/fix license headers --- .../jazzer/sanitizers/FilePathTraversal.java | 15 +++++++++++ .../java/com/example/FilePathTraversal.java | 27 ++++++++++--------- 2 files changed, 30 insertions(+), 12 deletions(-) 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 9ec5ea69f..b7f5d1fff 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 @@ -1,3 +1,18 @@ +/* + * Copyright 2024 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 com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical; diff --git a/sanitizers/src/test/java/com/example/FilePathTraversal.java b/sanitizers/src/test/java/com/example/FilePathTraversal.java index 1e21a47d1..f8718237a 100644 --- a/sanitizers/src/test/java/com/example/FilePathTraversal.java +++ b/sanitizers/src/test/java/com/example/FilePathTraversal.java @@ -1,15 +1,18 @@ -// -// 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. +/* + * Copyright 2024 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; From 87c1ae3d33c2b94ad317f282ff2ba26d33786ef9 Mon Sep 17 00:00:00 2001 From: Tim Allison Date: Tue, 7 Jan 2025 10:13:30 -0500 Subject: [PATCH 3/5] Update sanitizers/src/test/java/com/example/FilePathTraversal.java Co-authored-by: Peter Samarin --- .../java/com/example/FilePathTraversal.java | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/sanitizers/src/test/java/com/example/FilePathTraversal.java b/sanitizers/src/test/java/com/example/FilePathTraversal.java index f8718237a..441b5259f 100644 --- a/sanitizers/src/test/java/com/example/FilePathTraversal.java +++ b/sanitizers/src/test/java/com/example/FilePathTraversal.java @@ -16,6 +16,9 @@ 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; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -24,29 +27,22 @@ import java.nio.file.Path; import java.nio.file.Paths; -import com.code_intelligence.jazzer.api.FuzzedDataProvider; +public class FilePathTraversal { -public class FilePathTraversal { - - static { - System.setProperty("jazzer.fs_allowed_dirs", - System.getProperty("java.io.tmpdir") + "," + - "/a/b/c/allowed"); - } - - public static void fuzzerTestOneInput(FuzzedDataProvider fuzzedDataProvider) { - String data = fuzzedDataProvider.consumeString(100); - String path = fuzzedDataProvider.consumeProbabilityDouble() < 0.1 ? - "/a/b/d/e/jazzer-traversal" : data; - 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 - } + 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; + 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 } + } } From b4e3d7d02ca4d9546a4fd8087ed209a2f4d637d5 Mon Sep 17 00:00:00 2001 From: Tim Allison Date: Tue, 7 Jan 2025 10:14:28 -0500 Subject: [PATCH 4/5] Update sanitizers/src/test/java/com/example/BUILD.bazel Co-authored-by: Peter Samarin --- sanitizers/src/test/java/com/example/BUILD.bazel | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sanitizers/src/test/java/com/example/BUILD.bazel b/sanitizers/src/test/java/com/example/BUILD.bazel index 189fa0172..a7d63c403 100644 --- a/sanitizers/src/test/java/com/example/BUILD.bazel +++ b/sanitizers/src/test/java/com/example/BUILD.bazel @@ -69,6 +69,10 @@ java_fuzz_target_test( 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( From 7aab4359ff613351379cb20258d5a0e4e13c6af9 Mon Sep 17 00:00:00 2001 From: tallison Date: Tue, 14 Jan 2025 16:49:54 -0500 Subject: [PATCH 5/5] updates --- .../jazzer/sanitizers/FilePathTraversal.java | 596 +++++++++--------- .../example/AbsoluteFilePathTraversal.java | 52 ++ .../src/test/java/com/example/BUILD.bazel | 17 +- 3 files changed, 357 insertions(+), 308 deletions(-) create mode 100644 sanitizers/src/test/java/com/example/AbsoluteFilePathTraversal.java 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 b7f5d1fff..303125162 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 @@ -19,349 +19,331 @@ import com.code_intelligence.jazzer.api.HookType; import com.code_intelligence.jazzer.api.Jazzer; import com.code_intelligence.jazzer.api.MethodHook; - -import java.io.IOException; import java.io.File; import java.lang.invoke.MethodHandle; -import java.nio.file.Files; 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; /** - * This tests for a file read or write of a specific file name AND - * whether that file is in an allowed directory or a descendant. - *

- * This checks only for literal, absolute, normalized paths. It does not process symbolic links. - *

- * This sanitizer will only trigger if {@link FilePathTraversal#ALLOWED_DIRS_KEY} is - * set as an environment variable. If that is not set, this sanitizer is a no-op. - *

- * This does not check for reading metadata from files outside of the allowed directories. + * 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} + * + *

Users may customize a customize the target by setting the full path in the environment + * variable {@link FilePathTraversal#FILE_PATH_TARGET_KEY} + * + *

This does not currently check for reading metadata from the target file. */ public class FilePathTraversal { - public static final String FILE_NAME_ENV_KEY = "JAZZER_FILE_SYSTEM_TRAVERSAL_FILE_NAME"; - public static final String ALLOWED_DIRS_KEY = "jazzer.fs_allowed_dirs"; - public static final String DEFAULT_SENTINEL = "jazzer-traversal"; - public static final String SENTINEL = - (System.getenv(FILE_NAME_ENV_KEY) == null || - System.getenv(FILE_NAME_ENV_KEY).trim().length() == 0) ? - DEFAULT_SENTINEL : System.getenv(FILE_NAME_ENV_KEY); + public static final String FILE_PATH_TARGET_KEY = "jazzer.file_path_traversal_target"; + public static final String DEFAULT_TARGET_STRING = "../jazzer-traversal"; - //intentionally skipping createLink and createSymbolicLink + private static final Logger LOG = Logger.getLogger(FilePathTraversal.class.getName()); - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "createDirectory" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "createDirectories" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "createFile" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "createTempDirectory" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "createTempFile" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "delete" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "deleteIfExists" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "lines" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "newByteChannel" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "newBufferedReader" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "newBufferedWriter" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "readString" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "newBufferedReader" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "readAllBytes" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "readAllLines" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "readSymbolicLink" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "write" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "writeString" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "newInputStream" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "newOutputStream" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.probeContentType", - targetMethod = "open" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.channels.FileChannel", - targetMethod = "open" - ) - 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); - } - } - } + private static Path RELATIVE_TARGET; + private static Path ABSOLUTE_TARGET; + private static boolean IS_DISABLED = false; + private static boolean IS_SET_UP = false; - /** - * 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 - */ - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "copy" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "mismatch" - ) - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.nio.file.Files", - targetMethod = "move" - ) - 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); - } - Object to = arguments[1]; - if (to instanceof Path) { - checkPath((Path) to); - } - } + 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; + } + setTargets(DEFAULT_TARGET_STRING); } + } - - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.io.FileReader", - targetMethod = "" - ) - public static void fileReaderHook( - MethodHandle method, Object thisObject, Object[] arguments, int hookId) { - if (arguments.length > 0) { - - Object argObj = arguments[0]; - if (argObj instanceof String) { - checkPath((String)argObj); - } else if (argObj instanceof File) { - checkPath((File)argObj); - } - } + 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(); } + } - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.io.FileWriter", - targetMethod = "" - ) - public static void fileWriterHook( - MethodHandle method, Object thisObject, Object[] arguments, int hookId) { - if (arguments.length > 0) { - - Object argObj = arguments[0]; - if (argObj instanceof String) { - checkPath((String)argObj); - } else if (argObj instanceof File) { - checkPath((File)argObj); - } - } + // intentionally skipping createLink and createSymbolicLink + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createDirectory") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createDirectories") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createFile") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createTempDirectory") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "createTempFile") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "delete") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "deleteIfExists") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "lines") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newByteChannel") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newBufferedReader") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newBufferedWriter") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readString") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newBufferedReader") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readAllBytes") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readAllLines") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "readSymbolicLink") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "write") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "writeString") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newInputStream") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "newOutputStream") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.probeContentType", + targetMethod = "open") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.channels.FileChannel", + targetMethod = "open") + 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); + } } + } - - - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.io.FileInputStream", - targetMethod = "" - ) - public static void fileInputStreamHook( - MethodHandle method, Object thisObject, Object[] arguments, int hookId) { - if (arguments.length > 0) { - - Object argObj = arguments[0]; - if (argObj instanceof String) { - checkPath((String)argObj); - } else if (argObj instanceof File) { - checkPath((File)argObj); - } - } + /** + * 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 + */ + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "copy") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "mismatch") + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.nio.file.Files", + targetMethod = "move") + 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); + } } + } - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.io.FileOutputStream", - targetMethod = "" - ) - public static void processFileOutputStartHook( - MethodHandle method, Object thisObject, Object[] arguments, int hookId) { - if (arguments.length > 0) { - Object argObj = arguments[0]; - if (argObj instanceof File) { - if (argObj instanceof String) { - checkPath((String)argObj); - } else if (argObj instanceof File) { - checkPath((File)argObj); - } - } - } + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileReader", + targetMethod = "") + public static void fileReaderHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + checkObj(arguments[0], hookId); } + } - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.util.Scanner", - targetMethod = "" - ) - public static void scannerHook( - MethodHandle method, Object thisObject, Object[] arguments, int hookId) { - if (arguments.length > 0) { + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileWriter", + targetMethod = "") + public static void fileWriterHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + checkObj(arguments[0], hookId); + } + } - Object argObj = arguments[0]; - if (argObj instanceof String) { - checkPath((String)argObj); - } else if (argObj instanceof Path) { - checkPath((Path)argObj); - } else if (argObj instanceof File) { - checkPath((File)argObj); - } - } + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileInputStream", + targetMethod = "") + public static void fileInputStreamHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + checkObj(arguments[0], hookId); } + } - @MethodHook( - type = HookType.BEFORE, - targetClassName = "java.io.FileOutputStream", - targetMethod = "" - ) - public static void fileOutputStreamHook( - MethodHandle method, Object thisObject, Object[] arguments, int hookId) { - if (arguments.length > 0) { + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileOutputStream", + targetMethod = "") + public static void processFileOutputStartHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + checkObj(arguments[0], hookId); + } + } - Object argObj = arguments[0]; - if (argObj instanceof File) { - checkPath((File)argObj); - } else if (argObj instanceof String) { - checkPath((String)argObj); - } - } + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.util.Scanner", + targetMethod = "") + public static void scannerHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + checkObj(arguments[0], hookId); } + } - private static void checkPath(File f) { - try { - checkPath(f.toPath()); - } catch (InvalidPathException e) { - //TODO: give up -- for now - } + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.FileOutputStream", + targetMethod = "") + public static void fileOutputStreamHook( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + if (arguments.length > 0) { + checkObj(arguments[0], hookId); } + } - private static void checkPath(String s) { - try { - checkPath(Paths.get(s)); - } catch (InvalidPathException e) { - checkPath(new File(s)); - } + 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 checkPath(Path p) { - if (p.getFileName().toString().equals(SENTINEL) && ! isAllowed(p)) { - Jazzer.reportFindingFromHook( - new FuzzerSecurityIssueCritical("File path traversal: " + p)); - } + 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); } + } - private static boolean isAllowed(Path candidate) { - String allowedDirString = System.getProperty(ALLOWED_DIRS_KEY); + private static void checkFile(File f, int hookId) { + try { + check(f.toPath()); + } catch (InvalidPathException e) { + // TODO: give up -- for now + 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); + } + } - if (allowedDirString == null || allowedDirString.trim().length() == 0) { - return true; - } + 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 + return; + } + Path normalized = Paths.get(s); + if (normalized.isAbsolute()) { + Jazzer.guideTowardsEquality(s, ABSOLUTE_TARGET.toString(), hookId); + } else { + Jazzer.guideTowardsEquality(s, RELATIVE_TARGET.toString(), hookId); + } + } - Path candidateNormalized = candidate.toAbsolutePath().normalize(); - for (String pString : allowedDirString.split(",")) { - Path allowedNormalized = Paths.get(pString).toAbsolutePath().normalize(); - if (candidateNormalized.startsWith(allowedNormalized) && - ! candidateNormalized.equals(allowedNormalized)) { - return true; - } - } - return false; + 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) { + return; } + if (p.toAbsolutePath().normalize().equals(ABSOLUTE_TARGET)) { + Jazzer.reportFindingFromHook(new FuzzerSecurityIssueCritical("File path traversal: " + p)); + } + } } - diff --git a/sanitizers/src/test/java/com/example/AbsoluteFilePathTraversal.java b/sanitizers/src/test/java/com/example/AbsoluteFilePathTraversal.java new file mode 100644 index 000000000..f6a7648f5 --- /dev/null +++ b/sanitizers/src/test/java/com/example/AbsoluteFilePathTraversal.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 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.mutation.annotation.DoubleInRange; +import com.code_intelligence.jazzer.mutation.annotation.NotNull; +import com.code_intelligence.jazzer.mutation.annotation.WithUtf8Length; +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class AbsoluteFilePathTraversal { + static { + System.setProperty("jazzer.file_path_traversal_target", "/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; + + 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 a7d63c403..695380c79 100644 --- a/sanitizers/src/test/java/com/example/BUILD.bazel +++ b/sanitizers/src/test/java/com/example/BUILD.bazel @@ -58,6 +58,22 @@ java_fuzz_target_test( ], ) +java_fuzz_target_test( + name = "AbsoluteFilePathTraversal", + srcs = [ + "AbsoluteFilePathTraversal.java", + ], + allowed_findings = [ + "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", + ], +) + java_fuzz_target_test( name = "FilePathTraversal", srcs = [ @@ -73,7 +89,6 @@ java_fuzz_target_test( "//src/main/java/com/code_intelligence/jazzer/mutation/annotation", ], ) -) java_fuzz_target_test( name = "OsCommandInjectionProcessBuilder",