diff options
Diffstat (limited to 'sanitizers/src/main')
14 files changed, 557 insertions, 196 deletions
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 1b156f9e..c2521b80 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 @@ -1,12 +1,38 @@ +load("@bazel_skylib//rules:write_file.bzl", "write_file") load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") +load("//bazel:kotlin.bzl", "ktlint") +load("//sanitizers:sanitizers.bzl", "SANITIZER_CLASSES") java_library( name = "regex_roadblocks", srcs = ["RegexRoadblocks.java"], deps = [ - "//agent:jazzer_api_compile_only", - "//agent/src/main/java/com/code_intelligence/jazzer/runtime:unsafe_provider", "//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils:reflection_utils", + "//src/main/java/com/code_intelligence/jazzer/api:hooks", + "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider", + ], +) + +java_library( + name = "server_side_request_forgery", + srcs = ["ServerSideRequestForgery.java"], + deps = ["//src/main/java/com/code_intelligence/jazzer/api:hooks"], +) + +java_library( + name = "sql_injection", + srcs = ["SqlInjection.java"], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/api:hooks", + "@com_github_jsqlparser_jsqlparser//jar", + ], +) + +java_library( + name = "script_engine_injection", + srcs = ["ScriptEngineInjection.java"], + deps = [ + "//src/main/java/com/code_intelligence/jazzer/api:hooks", ], ) @@ -20,15 +46,38 @@ kt_jvm_library( "OsCommandInjection.kt", "ReflectiveCall.kt", "RegexInjection.kt", - "SqlInjection.kt", "Utils.kt", + "XPathInjection.kt", ], visibility = ["//sanitizers:__pkg__"], runtime_deps = [ ":regex_roadblocks", + ":script_engine_injection", + ":server_side_request_forgery", + ":sql_injection", ], deps = [ - "//agent:jazzer_api_compile_only", - "@maven//:com_github_jsqlparser_jsqlparser", + "//src/main/java/com/code_intelligence/jazzer/api:hooks", + ], +) + +java_library( + name = "constants", + srcs = [":constants_java"], + visibility = ["//visibility:public"], +) + +write_file( + name = "constants_java", + out = "Constants.java", + content = [ + "package com.code_intelligence.jazzer.sanitizers;", + "import java.util.Arrays;", + "import java.util.List;", + "public final class Constants {", + " public static final List<String> SANITIZER_HOOK_NAMES = Arrays.asList(%s);" % ", ".join(["\"%s\"" % name for name in SANITIZER_CLASSES]), + "}", ], ) + +ktlint() diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt index 55691c1a..0ecbbf9f 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt @@ -35,6 +35,12 @@ object Deserialization { private val OBJECT_INPUT_STREAM_HEADER = ObjectStreamConstants.STREAM_MAGIC.toBytes() + ObjectStreamConstants.STREAM_VERSION.toBytes() + init { + require(OBJECT_INPUT_STREAM_HEADER.size <= 64) { + "Object input stream header must fit in a table of recent compares entry (64 bytes)" + } + } + /** * Used to memoize the [InputStream] used to construct a given [ObjectInputStream]. * [ThreadLocal] is required because the map is not synchronized (and likely cheaper than @@ -57,13 +63,19 @@ object Deserialization { // We can't instantiate jaz.Zer directly, so we instantiate and serialize jaz.Ter and then // patch the class name. val baos = ByteArrayOutputStream() - ObjectOutputStream(baos).writeObject(jaz.Ter()) + ObjectOutputStream(baos).writeObject(jaz.Ter(jaz.Ter.EXPRESSION_LANGUAGE_SANITIZER_ID)) val serializedJazTerInstance = baos.toByteArray() val posToPatch = serializedJazTerInstance.indexOf("jaz.Ter".toByteArray()) serializedJazTerInstance[posToPatch + "jaz.".length] = 'Z'.code.toByte() serializedJazTerInstance } + init { + require(SERIALIZED_JAZ_ZER_INSTANCE.size <= 64) { + "Serialized jaz.Zer instance must fit in a table of recent compares entry (64 bytes)" + } + } + /** * Guides the fuzzer towards producing a valid header for an ObjectInputStream. */ @@ -71,15 +83,16 @@ object Deserialization { type = HookType.BEFORE, targetClassName = "java.io.ObjectInputStream", targetMethod = "<init>", - targetMethodDescriptor = "(Ljava/io/InputStream;)V" + targetMethodDescriptor = "(Ljava/io/InputStream;)V", ) @JvmStatic fun objectInputStreamInitBeforeHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { val originalInputStream = args[0] as? InputStream ?: return - val fixedInputStream = if (originalInputStream.markSupported()) + val fixedInputStream = if (originalInputStream.markSupported()) { originalInputStream - else + } else { BufferedInputStream(originalInputStream) + } args[0] = fixedInputStream guideMarkableInputStreamTowardsEquality(fixedInputStream, OBJECT_INPUT_STREAM_HEADER, hookId) } @@ -91,7 +104,7 @@ object Deserialization { type = HookType.AFTER, targetClassName = "java.io.ObjectInputStream", targetMethod = "<init>", - targetMethodDescriptor = "(Ljava/io/InputStream;)V" + targetMethodDescriptor = "(Ljava/io/InputStream;)V", ) @JvmStatic fun objectInputStreamInitAfterHook( @@ -115,17 +128,17 @@ object Deserialization { MethodHook( type = HookType.BEFORE, targetClassName = "java.io.ObjectInputStream", - targetMethod = "readObject" + targetMethod = "readObject", ), MethodHook( type = HookType.BEFORE, targetClassName = "java.io.ObjectInputStream", - targetMethod = "readObjectOverride" + targetMethod = "readObjectOverride", ), MethodHook( type = HookType.BEFORE, targetClassName = "java.io.ObjectInputStream", - targetMethod = "readUnshared" + targetMethod = "readUnshared", ), ) @JvmStatic @@ -139,30 +152,4 @@ object Deserialization { if (inputStream?.markSupported() != true) return guideMarkableInputStreamTowardsEquality(inputStream, SERIALIZED_JAZ_ZER_INSTANCE, hookId) } - - /** - * Calls [Object.finalize] early if the returned object is [jaz.Zer]. A call to finalize is - * guaranteed to happen at some point, but calling it early means that we can accurately report - * the input that lead to its execution. - */ - @MethodHooks( - MethodHook(type = HookType.AFTER, targetClassName = "java.io.ObjectInputStream", targetMethod = "readObject"), - MethodHook(type = HookType.AFTER, targetClassName = "java.io.ObjectInputStream", targetMethod = "readObjectOverride"), - MethodHook(type = HookType.AFTER, targetClassName = "java.io.ObjectInputStream", targetMethod = "readUnshared"), - ) - @JvmStatic - fun readObjectAfterHook( - method: MethodHandle?, - objectInputStream: ObjectInputStream?, - args: Array<Any?>, - hookId: Int, - deserializedObject: Any?, - ) { - if (deserializedObject?.javaClass?.name == HONEYPOT_CLASS_NAME) { - deserializedObject.javaClass.getDeclaredMethod("finalize").run { - isAccessible = true - invoke(deserializedObject) - } - } - } } diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt index 1dc1d5f0..a60c088e 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt @@ -31,7 +31,13 @@ object ExpressionLanguageInjection { * Try to call the default constructor of the honeypot class. */ private const val EXPRESSION_LANGUAGE_ATTACK = - "\${\"\".getClass().forName(\"$HONEYPOT_CLASS_NAME\").newInstance()}" + "\${Byte.class.forName(\"$HONEYPOT_CLASS_NAME\").getMethod(\"el\").invoke(null)}" + + init { + require(EXPRESSION_LANGUAGE_ATTACK.length <= 64) { + "Expression language exploit must fit in a table of recent compares entry (64 bytes)" + } + } @MethodHooks( MethodHook( @@ -60,8 +66,10 @@ object ExpressionLanguageInjection { method: MethodHandle?, thisObject: Any?, arguments: Array<Any>, - hookId: Int + hookId: Int, ) { + // The overloads taking a second string argument have either three or four arguments + if (arguments.size < 3) { return } val expression = arguments[1] as? String ?: return Jazzer.guideTowardsContainment(expression, EXPRESSION_LANGUAGE_ATTACK, hookId) } @@ -76,15 +84,16 @@ object ExpressionLanguageInjection { @MethodHook( type = HookType.BEFORE, targetClassName = "javax.validation.ConstraintValidatorContext", - targetMethod = "buildConstraintViolationWithTemplate" + targetMethod = "buildConstraintViolationWithTemplate", ) @JvmStatic fun hookBuildConstraintViolationWithTemplate( method: MethodHandle?, thisObject: Any?, arguments: Array<Any>, - hookId: Int + hookId: Int, ) { + if (arguments.size != 1) { return } val message = arguments[0] as String Jazzer.guideTowardsContainment(message, EXPRESSION_LANGUAGE_ATTACK, hookId) } diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt index 1afd614e..76553e1a 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt @@ -56,14 +56,14 @@ object LdapInjection { targetClassName = "javax.naming.directory.DirContext", targetMethod = "search", targetMethodDescriptor = "(Ljava/lang/String;Ljavax/naming.directory/Attributes;)Ljavax/naming/NamingEnumeration;", - additionalClassesToHook = ["javax.naming.directory.InitialDirContext"] + additionalClassesToHook = ["javax.naming.directory.InitialDirContext"], ), MethodHook( type = HookType.REPLACE, targetClassName = "javax.naming.directory.DirContext", targetMethod = "search", targetMethodDescriptor = "(Ljava/lang/String;Ljavax/naming.directory/Attributes;[Ljava/lang/Sting;)Ljavax/naming/NamingEnumeration;", - additionalClassesToHook = ["javax.naming.directory.InitialDirContext"] + additionalClassesToHook = ["javax.naming.directory.InitialDirContext"], ), // Object search, possible DN and search filter injection @@ -72,22 +72,22 @@ object LdapInjection { targetClassName = "javax.naming.directory.DirContext", targetMethod = "search", targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/String;Ljavax/naming/directory/SearchControls;)Ljavax/naming/NamingEnumeration;", - additionalClassesToHook = ["javax.naming.directory.InitialDirContext"] + additionalClassesToHook = ["javax.naming.directory.InitialDirContext"], ), MethodHook( type = HookType.REPLACE, targetClassName = "javax.naming.directory.DirContext", targetMethod = "search", targetMethodDescriptor = "(Ljavax/naming/Name;Ljava/lang/String;[Ljava.lang.Object;Ljavax/naming/directory/SearchControls;)Ljavax/naming/NamingEnumeration;", - additionalClassesToHook = ["javax.naming.directory.InitialDirContext"] + additionalClassesToHook = ["javax.naming.directory.InitialDirContext"], ), MethodHook( type = HookType.REPLACE, targetClassName = "javax.naming.directory.DirContext", targetMethod = "search", targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;Ljavax/naming/directory/SearchControls;)Ljavax/naming/NamingEnumeration;", - additionalClassesToHook = ["javax.naming.directory.InitialDirContext"] - ) + additionalClassesToHook = ["javax.naming.directory.InitialDirContext"], + ), ) @JvmStatic fun searchLdapContext(method: MethodHandle, thisObject: Any?, args: Array<Any>, hookId: Int): Any? { @@ -106,15 +106,15 @@ object LdapInjection { Jazzer.reportFindingFromHook( FuzzerSecurityIssueCritical( """LDAP Injection -Search filters based on untrusted data must be escape as specified in RFC 4515.""" - ) +Search filters based on untrusted data must be escaped as specified in RFC 4515.""", + ), ) is NamingException -> Jazzer.reportFindingFromHook( FuzzerSecurityIssueCritical( """LDAP Injection -Distinguished Names based on untrusted data must be escaped as specified in RFC 2253.""" - ) +Distinguished Names based on untrusted data must be escaped as specified in RFC 2253.""", + ), ) } throw e diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt index 56e12f03..51cf6453 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt @@ -49,14 +49,14 @@ object NamingContextLookup { ) @JvmStatic fun lookupHook(method: MethodHandle?, thisObject: Any?, args: Array<Any?>, hookId: Int): Any { - val name = args[0] as String + val name = args[0] as? String ?: throw CommunicationException() if (name.startsWith(RMI_MARKER) || name.startsWith(LDAP_MARKER)) { Jazzer.reportFindingFromHook( FuzzerSecurityIssueCritical( """Remote JNDI Lookup JNDI lookups with attacker-controlled remote URLs can, depending on the JDK -version, lead to remote code execution or the exfiltration of information.""" - ) +version, lead to remote code execution or the exfiltration of information.""", + ), ) } Jazzer.guideTowardsEquality(name, RMI_MARKER, hookId) diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt index d3adc207..87de35c7 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt @@ -39,10 +39,11 @@ object OsCommandInjection { type = HookType.BEFORE, targetClassName = "java.lang.ProcessImpl", targetMethod = "start", - additionalClassesToHook = ["java.lang.ProcessBuilder"] + additionalClassesToHook = ["java.lang.ProcessBuilder"], ) @JvmStatic fun processImplStartHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + if (args.isEmpty()) { return } // Calling ProcessBuilder already checks if command array is empty @Suppress("UNCHECKED_CAST") (args[0] as? Array<String>)?.first().let { cmd -> @@ -50,8 +51,8 @@ object OsCommandInjection { Jazzer.reportFindingFromHook( FuzzerSecurityIssueCritical( """OS Command Injection -Executing OS commands with attacker-controlled data can lead to remote code execution.""" - ) +Executing OS commands with attacker-controlled data can lead to remote code execution.""", + ), ) } else { Jazzer.guideTowardsEquality(cmd, COMMAND, hookId) diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt index 0fcabe36..62d58152 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt @@ -61,10 +61,11 @@ object ReflectiveCall { ) @JvmStatic fun loadLibraryHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + if (args.isEmpty()) { return } val libraryName = args[0] as? String ?: return if (libraryName == HONEYPOT_LIBRARY_NAME) { Jazzer.reportFindingFromHook( - FuzzerSecurityIssueHigh("load arbitrary library") + FuzzerSecurityIssueHigh("load arbitrary library"), ) } Jazzer.guideTowardsEquality(libraryName, HONEYPOT_LIBRARY_NAME, hookId) diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt index def5f6e3..5770f0c2 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt @@ -23,6 +23,9 @@ import java.lang.invoke.MethodHandle import java.util.regex.Pattern import java.util.regex.PatternSyntaxException +// message introduced in JDK14 and ported back to previous versions +private const val STACK_OVERFLOW_ERROR_MESSAGE = "Stack overflow during pattern compilation" + @Suppress("unused_parameter", "unused") object RegexInjection { /** @@ -43,7 +46,7 @@ object RegexInjection { type = HookType.REPLACE, targetClassName = "java.util.regex.Pattern", targetMethod = "compile", - targetMethodDescriptor = "(Ljava/lang/String;I)Ljava/util/regex/Pattern;" + targetMethodDescriptor = "(Ljava/lang/String;I)Ljava/util/regex/Pattern;", ) @JvmStatic fun compileWithFlagsHook(method: MethodHandle, alwaysNull: Any?, args: Array<Any?>, hookId: Int): Any? { @@ -57,13 +60,13 @@ object RegexInjection { type = HookType.REPLACE, targetClassName = "java.util.regex.Pattern", targetMethod = "compile", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/util/regex/Pattern;" + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/util/regex/Pattern;", ), MethodHook( type = HookType.REPLACE, targetClassName = "java.util.regex.Pattern", targetMethod = "matches", - targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/CharSequence;)Z" + targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/CharSequence;)Z", ), ) @JvmStatic @@ -113,7 +116,7 @@ object RegexInjection { pattern: String?, hasCanonEqFlag: Boolean, hookId: Int, - vararg args: Any? + vararg args: Any?, ): Any? { if (hasCanonEqFlag && pattern != null) { // With CANON_EQ enabled, Pattern.compile allocates an array with a size that is @@ -128,8 +131,8 @@ object RegexInjection { """Regular Expression Injection with CANON_EQ When java.util.regex.Pattern.compile is used with the Pattern.CANON_EQ flag, every injection into the regular expression pattern can cause arbitrarily large -memory allocations, even when wrapped with Pattern.quote(...).""" - ) +memory allocations, even when wrapped with Pattern.quote(...).""", + ), ) } else { Jazzer.guideTowardsContainment(pattern, CANON_EQ_ALMOST_EXPLOIT, hookId) @@ -143,15 +146,15 @@ memory allocations, even when wrapped with Pattern.quote(...).""" } } } catch (e: Exception) { - if (e is PatternSyntaxException) { + if (e is PatternSyntaxException && !(e.message ?: "").startsWith(STACK_OVERFLOW_ERROR_MESSAGE)) { Jazzer.reportFindingFromHook( FuzzerSecurityIssueLow( """Regular Expression Injection Regular expression patterns that contain unescaped untrusted input can consume arbitrary amounts of CPU time. To properly escape the input, wrap it with Pattern.quote(...).""", - e - ) + e, + ), ) } throw e diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java index 1043ac02..76c499b0 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java @@ -22,7 +22,7 @@ import static com.code_intelligence.jazzer.sanitizers.utils.ReflectionUtils.offs import com.code_intelligence.jazzer.api.HookType; import com.code_intelligence.jazzer.api.Jazzer; import com.code_intelligence.jazzer.api.MethodHook; -import com.code_intelligence.jazzer.runtime.UnsafeProvider; +import com.code_intelligence.jazzer.utils.UnsafeProvider; import java.lang.invoke.MethodHandle; import java.util.WeakHashMap; import java.util.regex.Matcher; @@ -65,13 +65,6 @@ public final class RegexRoadblocks { private static final ThreadLocal<WeakHashMap<Object, Character>> PREDICATE_SOLUTIONS = ThreadLocal.withInitial(WeakHashMap::new); - // Do not act on instrumented regexes used by Jazzer internally, e.g. by ClassGraph. - private static boolean HOOK_DISABLED = true; - - static { - Jazzer.onFuzzTargetReady(() -> HOOK_DISABLED = UNSAFE == null); - } - @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$Node", targetMethod = "match", targetMethodDescriptor = "(Ljava/util/regex/Matcher;ILjava/lang/CharSequence;)Z", @@ -122,7 +115,7 @@ public final class RegexRoadblocks { }) public static void nodeMatchHook(MethodHandle method, Object node, Object[] args, int hookId, Boolean matched) { - if (HOOK_DISABLED || matched || node == null) + if (matched || node == null) return; Matcher matcher = (Matcher) args[0]; if (matcher == null) @@ -211,7 +204,7 @@ public final class RegexRoadblocks { additionalClassesToHook = {"java.util.regex.Pattern"}) public static void singleHook(MethodHandle method, Object node, Object[] args, int hookId, Object predicate) { - if (HOOK_DISABLED || predicate == null) + if (predicate == null) return; PREDICATE_SOLUTIONS.get().put(predicate, (char) (int) args[0]); } @@ -229,7 +222,7 @@ public final class RegexRoadblocks { public static void java8SingleHook( MethodHandle method, Object property, Object[] args, int hookId, Object alwaysNull) { - if (HOOK_DISABLED || property == null) + if (property == null) return; PREDICATE_SOLUTIONS.get().put(property, (char) (int) args[0]); } @@ -258,7 +251,7 @@ public final class RegexRoadblocks { additionalClassesToHook = {"java.util.regex.Pattern"}) public static void rangeHook(MethodHandle method, Object node, Object[] args, int hookId, Object predicate) { - if (HOOK_DISABLED || predicate == null) + if (predicate == null) return; PREDICATE_SOLUTIONS.get().put(predicate, (char) (int) args[0]); } @@ -280,7 +273,7 @@ public final class RegexRoadblocks { public static void unionHook( MethodHandle method, Object thisObject, Object[] args, int hookId, Object unionPredicate) { - if (HOOK_DISABLED || unionPredicate == null) + if (unionPredicate == null) return; Character solution = predicateSolution(thisObject); if (solution == null) @@ -298,7 +291,6 @@ public final class RegexRoadblocks { boolean[] bits = (boolean[]) UNSAFE.getObject(charPredicate, BIT_CLASS_BITS_OFFSET); for (int i = 0; i < bits.length; i++) { if (bits[i]) { - PREDICATE_SOLUTIONS.get().put(charPredicate, (char) i); return (char) i; } } diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ScriptEngineInjection.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ScriptEngineInjection.java new file mode 100644 index 00000000..6f084bf9 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ScriptEngineInjection.java @@ -0,0 +1,108 @@ +// Copyright 2023 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; +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.Reader; +import java.io.StringReader; +import java.lang.invoke.MethodHandle; + +/** + * Detects Script Engine injections. + * + * <p> + * The hooks in this class attempt to detect user input flowing into + * {@link javax.script.ScriptEngine#eval(String)} and the like that might lead + * to remote code executions depending on the scripting engine's capabilities. + * Before JDK 15, the Nashorn Engine was registered by default with + * ScriptEngineManager under several aliases, including "js". Nashorn allows + * access to JVM classes, for example {@link java.lang.Runtime} allowing the + * execution of arbitrary OS commands. Several other scripting engines can be + * embedded to the JVM (they must follow the + * <a href="https://www.jcp.org/en/jsr/detail?id=223">JSR-223 </a> + * specification). + **/ +@SuppressWarnings("unused") +public final class ScriptEngineInjection { + private static final String PAYLOAD = "\"jaz\"+\"zer\""; + + /** + * String variants of eval can be intercepted by before hooks, as the script + * content can directly be checked for the presence of the payload. + */ + @MethodHook(type = HookType.BEFORE, targetClassName = "javax.script.ScriptEngine", + targetMethod = "eval", targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;") + @MethodHook(type = HookType.BEFORE, targetClassName = "javax.script.ScriptEngine", + targetMethod = "eval", + targetMethodDescriptor = "(Ljava/lang/String;Ljavax/script/ScriptContext;)Ljava/lang/Object;") + @MethodHook(type = HookType.BEFORE, targetClassName = "javax.script.ScriptEngine", + targetMethod = "eval", + targetMethodDescriptor = "(Ljava/lang/String;Ljavax/script/Bindings;)Ljava/lang/Object;") + public static void + checkScriptEngineExecuteString( + MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + checkScriptContent((String) arguments[0], hookId); + } + + /** + * Reader variants of eval must be intercepted by replace hooks, as their + * contents are converted to strings, for the payload check, and back to readers + * for the actual method invocation. + */ + @MethodHook(type = HookType.REPLACE, targetClassName = "javax.script.ScriptEngine", + targetMethod = "eval", targetMethodDescriptor = "(Ljava/io/Reader;)Ljava/lang/Object;") + @MethodHook(type = HookType.REPLACE, targetClassName = "javax.script.ScriptEngine", + targetMethod = "eval", + targetMethodDescriptor = "(Ljava/io/Reader;Ljavax/script/ScriptContext;)Ljava/lang/Object;") + @MethodHook(type = HookType.REPLACE, targetClassName = "javax.script.ScriptEngine", + targetMethod = "eval", + targetMethodDescriptor = "(Ljava/io/Reader;Ljavax/script/Bindings;)Ljava/lang/Object;") + public static Object + checkScriptEngineExecute(MethodHandle method, Object thisObject, Object[] arguments, int hookId) + throws Throwable { + if (arguments[0] != null) { + String content = readAll((Reader) arguments[0]); + checkScriptContent(content, hookId); + arguments[0] = new StringReader(content); + } + return method.invokeWithArguments(thisObject, arguments); + } + + private static void checkScriptContent(String content, int hookId) { + if (content != null) { + if (content.contains(PAYLOAD)) { + Jazzer.reportFindingFromHook(new FuzzerSecurityIssueCritical( + "Script Engine Injection: Insecure user input was used in script engine invocation.\n" + + "Depending on the script engine's capabilities this could lead to sandbox escape and remote code execution.")); + } else { + Jazzer.guideTowardsContainment(content, PAYLOAD, hookId); + } + } + } + + private static String readAll(Reader reader) throws IOException { + StringBuilder content = new StringBuilder(); + char[] buffer = new char[4096]; + int numChars; + while ((numChars = reader.read(buffer)) >= 0) { + content.append(buffer, 0, numChars); + } + return content.toString(); + } +} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ServerSideRequestForgery.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ServerSideRequestForgery.java new file mode 100644 index 00000000..3ff48e3c --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ServerSideRequestForgery.java @@ -0,0 +1,127 @@ +/* + * Copyright 2023 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.FuzzerSecurityIssueHigh; +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium; +import com.code_intelligence.jazzer.api.HookType; +import com.code_intelligence.jazzer.api.Jazzer; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiPredicate; + +public class ServerSideRequestForgery { + // Set via reflection by Jazzer's BugDetectors API. + public static final AtomicReference<BiPredicate<String, Integer>> connectionPermitted = + new AtomicReference<>((host, port) -> false); + + /** + * {@link java.net.Socket} is used in many JDK classes to open network connections. Internally it + * delegates to {@link java.net.SocketImpl}, hence, for most situations it's sufficient to hook + * the call site {@link java.net.Socket} itself. As {@link java.net.SocketImpl} is an abstract + * class all call sites invoking "connect" on concrete implementations get hooked. As JKD internal + * classes are normally ignored, they have to be marked for hooking explicitly. In this case, all + * internal classes calling "connect" on {@link java.net.SocketImpl} should be listed below. + * Internal classes using {@link java.net.SocketImpl#connect(String, int)}: + * <ul> + * <li>java.net.Socket (hook required) + * <li>java.net.AbstractPlainSocketImpl (no direct usage, no hook required) + * <li>java.net.PlainSocketImpl (no direct usage, no hook required) + * <li>java.net.HttpConnectSocketImpl (only used in Socket, which is already listed) + * <li>java.net.SocksSocketImpl (used in Socket, but also invoking super.connect directly, + * hook required) + * <li>java.net.ServerSocket (security check, no hook required) + * </ul> + */ + @MethodHook(type = HookType.BEFORE, targetClassName = "java.net.SocketImpl", + targetMethod = "connect", + additionalClassesToHook = + { + "java.net.Socket", + "java.net.SocksSocketImpl", + }) + public static void + checkSsrfSocket(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + checkSsrf(arguments); + } + + /** + * {@link java.nio.channels.SocketChannel} is used in many JDK classes to open (non-blocking) + * network connections, e.g. {@link java.net.http.HttpClient} uses it internally. The actual + * connection is established in the abstract "connect" method. Hooking that also hooks invocations + * of all concrete implementations, from which only one exists in {@link + * sun.nio.ch.SocketChannelImpl}. "connect" is only called in {@link + * java.nio.channels.SocketChannel} itself and the two mentioned classes below. + */ + @MethodHook(type = HookType.BEFORE, targetClassName = "java.nio.channels.SocketChannel", + targetMethod = "connect", + additionalClassesToHook = + { + "sun.nio.ch.SocketAdaptor", + "jdk.internal.net.http.PlainHttpConnection", + }) + public static void + checkSsrfHttpConnection(MethodHandle method, Object thisObject, Object[] arguments, int hookId) { + checkSsrf(arguments); + } + + private static void checkSsrf(Object[] arguments) { + if (arguments.length == 0) { + return; + } + + String host; + int port; + if (arguments[0] instanceof InetSocketAddress) { + // Only implementation of java.net.SocketAddress. + InetSocketAddress address = (InetSocketAddress) arguments[0]; + host = address.getHostName(); + port = address.getPort(); + } else if (arguments.length >= 2 && arguments[1] instanceof Integer) { + if (arguments[0] instanceof InetAddress) { + host = ((InetAddress) arguments[0]).getHostName(); + } else if (arguments[0] instanceof String) { + host = (String) arguments[0]; + } else { + return; + } + port = (int) arguments[1]; + } else { + return; + } + + if (port < 0 || port > 65535) { + return; + } + + if (!connectionPermitted.get().test(host, port)) { + Jazzer.reportFindingFromHook(new FuzzerSecurityIssueMedium(String.format( + "Server Side Request Forgery (SSRF)\n" + + "Attempted connection to: %s:%d\n" + + "Requests to destinations based on untrusted data could lead to exfiltration of " + + "sensitive data or exposure of internal services.\n\n" + + "If the fuzz test is expected to perform network connections, call " + + "com.code_intelligence.jazzer.api.BugDetectors#allowNetworkConnections at the " + + "beginning of your fuzz test and optionally provide a predicate matching the " + + "expected hosts.", + host, port))); + } + } +} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.java new file mode 100644 index 00000000..da5beaa8 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.java @@ -0,0 +1,119 @@ +// Copyright 2022 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 java.util.Collections.unmodifiableSet; +import static java.util.stream.Collectors.toSet; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh; +import com.code_intelligence.jazzer.api.HookType; +import com.code_intelligence.jazzer.api.Jazzer; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; + +/** + * Detects SQL injections. + * + * <p>Untrusted input has to be escaped in such a way that queries remain valid otherwise an + * injection could be possible. This sanitizer guides the fuzzer to inject insecure characters. If + * an exception is raised during execution the fuzzer was able to inject an invalid pattern, + * otherwise all input was escaped correctly. + * + * <p>Two types of methods are hooked: + * <ol> + * <li>Methods that take an SQL query as the first argument (e.g. {@link + * java.sql.Statement#execute}]).</li> + * <li>Methods that don't take any arguments and execute an already prepared statement (e.g. {@link + * java.sql.PreparedStatement#execute}).</li> + * </ol> + * + * For 1. we validate the syntax of the query using <a + * href="https://github.com/JSQLParser/JSqlParser">jsqlparser</a> and if both the syntax is invalid + * and the query execution throws an exception we report an SQL injection. Since we can't reliably + * validate SQL queries in arbitrary dialects this hook is expected to produce some amount of false + * positives. For 2. we can't validate the query syntax and therefore only rethrow any exceptions. + */ +@SuppressWarnings("unused") +public class SqlInjection { + // Characters that should be escaped in user input. + // See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html + private static final String CHARACTERS_TO_ESCAPE = "'\"\b\n\r\t\\%_"; + + private static final Set<String> SQL_SYNTAX_ERROR_EXCEPTIONS = unmodifiableSet( + Stream + .of("java.sql.SQLException", "java.sql.SQLNonTransientException", + "java.sql.SQLSyntaxErrorException", "org.h2.jdbc.JdbcSQLSyntaxErrorException", + "org.h2.jdbc.JdbcSQLFeatureNotSupportedException") + .collect(toSet())); + + @MethodHook( + type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "execute") + @MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", + targetMethod = "executeBatch") + @MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", + targetMethod = "executeLargeBatch") + @MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", + targetMethod = "executeLargeUpdate") + @MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", + targetMethod = "executeQuery") + @MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", + targetMethod = "executeUpdate") + @MethodHook(type = HookType.REPLACE, targetClassName = "javax.persistence.EntityManager", + targetMethod = "createNativeQuery") + public static Object + checkSqlExecute(MethodHandle method, Object thisObject, Object[] arguments, int hookId) + throws Throwable { + boolean hasValidSqlQuery = false; + + if (arguments.length > 0 && arguments[0] instanceof String) { + String query = (String) arguments[0]; + hasValidSqlQuery = isValidSql(query); + Jazzer.guideTowardsContainment(query, CHARACTERS_TO_ESCAPE, hookId); + } + try { + return method.invokeWithArguments( + Stream.concat(Stream.of(thisObject), Arrays.stream(arguments)).toArray()); + } catch (Throwable throwable) { + // If we already validated the query string and know it's correct, + // The exception is likely thrown by a non-existent table or something + // that we don't want to report. + if (!hasValidSqlQuery + && SQL_SYNTAX_ERROR_EXCEPTIONS.contains(throwable.getClass().getName())) { + Jazzer.reportFindingFromHook(new FuzzerSecurityIssueHigh( + String.format("SQL Injection%nInjected query: %s%n", arguments[0]))); + } + throw throwable; + } + } + + private static boolean isValidSql(String sql) { + try { + CCJSqlParserUtil.parseStatements(sql); + return true; + } catch (JSQLParserException e) { + return false; + } catch (Throwable t) { + // Catch any unexpected exceptions so that we don't disturb the + // instrumented application. + t.printStackTrace(); + return true; + } + } +} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.kt deleted file mode 100644 index f317bcc8..00000000 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.kt +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2022 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.FuzzerSecurityIssueHigh -import com.code_intelligence.jazzer.api.HookType -import com.code_intelligence.jazzer.api.Jazzer -import com.code_intelligence.jazzer.api.MethodHook -import com.code_intelligence.jazzer.api.MethodHooks -import net.sf.jsqlparser.JSQLParserException -import net.sf.jsqlparser.parser.CCJSqlParserUtil -import java.lang.invoke.MethodHandle - -/** - * Detects SQL injections. - * - * Untrusted input has to be escaped in such a way that queries remain valid otherwise an injection - * could be possible. This sanitizer guides the fuzzer to inject insecure characters. If an exception - * is raised during execution the fuzzer was able to inject an invalid pattern, otherwise all input - * was escaped correctly. - * - * Two types of methods are hooked: - * 1. Methods that take an SQL query as the first argument (e.g. [java.sql.Statement.execute]). - * 2. Methods that don't take any arguments and execute an already prepared statement - * (e.g. [java.sql.PreparedStatement.execute]). - * For 1. we validate the syntax of the query using <a href="https://github.com/JSQLParser/JSqlParser">jsqlparser</a> - * and if both the syntax is invalid and the query execution throws an exception we report an SQL injection. - * Since we can't reliably validate SQL queries in arbitrary dialects this hook is expected to produce some - * amount of false positives. - * For 2. we can't validate the query syntax and therefore only rethrow any exceptions. - */ -@Suppress("unused_parameter", "unused") -object SqlInjection { - - // Characters that should be escaped in user input. - // See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html - private const val CHARACTERS_TO_ESCAPE = "'\"\b\n\r\t\\%_" - - private val SQL_SYNTAX_ERROR_EXCEPTIONS = listOf( - "java.sql.SQLException", - "java.sql.SQLNonTransientException", - "java.sql.SQLSyntaxErrorException", - "org.h2.jdbc.JdbcSQLSyntaxErrorException", - ) - - @MethodHooks( - MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "execute"), - MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeBatch"), - MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeLargeBatch"), - MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeLargeUpdate"), - MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeQuery"), - MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeUpdate"), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.persistence.EntityManager", - targetMethod = "createNativeQuery" - ) - ) - @JvmStatic - fun checkSqlExecute(method: MethodHandle, thisObject: Any?, arguments: Array<Any>, hookId: Int): Any { - var hasValidSqlQuery = false - - if (arguments.isNotEmpty() && arguments[0] is String) { - val query = arguments[0] as String - hasValidSqlQuery = isValidSql(query) - Jazzer.guideTowardsContainment(query, CHARACTERS_TO_ESCAPE, hookId) - } - return try { - method.invokeWithArguments(thisObject, *arguments) - } catch (throwable: Throwable) { - // If we already validated the query string and know it's correct, - // The exception is likely thrown by a non-existent table or something - // that we don't want to report. - if (!hasValidSqlQuery && SQL_SYNTAX_ERROR_EXCEPTIONS.contains(throwable.javaClass.name)) { - Jazzer.reportFindingFromHook( - FuzzerSecurityIssueHigh( - """ - SQL Injection - Injected query: ${arguments[0]} - """.trimIndent(), - throwable - ) - ) - } - throw throwable - } - } - - private fun isValidSql(sql: String): Boolean = - try { - CCJSqlParserUtil.parseStatements(sql) - true - } catch (e: JSQLParserException) { - false - } catch (t: Throwable) { - // Catch any unexpected exceptions so that we don't disturb the - // instrumented application. - t.printStackTrace() - true - } -} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/XPathInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/XPathInjection.kt new file mode 100644 index 00000000..b54d0839 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/XPathInjection.kt @@ -0,0 +1,78 @@ +// Copyright 2022 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.FuzzerSecurityIssueHigh +import com.code_intelligence.jazzer.api.HookType +import com.code_intelligence.jazzer.api.Jazzer +import com.code_intelligence.jazzer.api.MethodHook +import com.code_intelligence.jazzer.api.MethodHooks +import java.lang.invoke.MethodHandle +import javax.xml.xpath.XPathExpressionException + +/** + * Detects XPath injections. + * + * Untrusted input has to be escaped in such a way that queries remain valid, otherwise an injection + * could be possible. This sanitizer guides the fuzzer to inject insecure characters. If an exception + * is raised during execution the fuzzer was able to inject an invalid pattern, otherwise all input + * was escaped correctly. + * Checking if the innermost cause of XPathExpressionException is a TransformerException should + * indicate injection instead of a false positive. + */ +@Suppress("unused_parameter", "unused") +object XPathInjection { + + // Characters that should be escaped in user input. + // https://owasp.org/www-community/attacks/XPATH_Injection + private const val CHARACTERS_TO_ESCAPE = "'\"" + + private val XPATH_SYNTAX_ERROR_EXCEPTIONS = "javax.xml.transform.TransformerException" + + @MethodHooks( + MethodHook(type = HookType.REPLACE, targetClassName = "javax.xml.xpath.XPath", targetMethod = "compile"), + MethodHook(type = HookType.REPLACE, targetClassName = "javax.xml.xpath.XPath", targetMethod = "evaluate"), + MethodHook(type = HookType.REPLACE, targetClassName = "javax.xml.xpath.XPath", targetMethod = "evaluateExpression"), + ) + @JvmStatic + fun checkXpathExecute(method: MethodHandle, thisObject: Any?, arguments: Array<Any>, hookId: Int): Any { + if (arguments.isNotEmpty() && arguments[0] is String) { + val query = arguments[0] as String + Jazzer.guideTowardsContainment(query, CHARACTERS_TO_ESCAPE, hookId) + } + return try { + method.invokeWithArguments(thisObject, *arguments) + } catch (exception: XPathExpressionException) { + // find innermost cause + var innerCause = exception.cause + while (innerCause?.cause != null && innerCause.cause != innerCause) { + innerCause = innerCause.cause + } + + if (innerCause != null && XPATH_SYNTAX_ERROR_EXCEPTIONS.equals(innerCause.javaClass.name)) { + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueHigh( + """ + XPath Injection + Injected query: ${arguments[0]} + """.trimIndent(), + exception, + ), + ) + } + throw exception + } + } +} |