diff options
Diffstat (limited to 'sanitizers/src')
31 files changed, 1297 insertions, 268 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 + } + } +} diff --git a/sanitizers/src/test/java/com/example/BUILD.bazel b/sanitizers/src/test/java/com/example/BUILD.bazel index 5d2e1ca5..ea0a7f82 100644 --- a/sanitizers/src/test/java/com/example/BUILD.bazel +++ b/sanitizers/src/test/java/com/example/BUILD.bazel @@ -6,7 +6,10 @@ java_fuzz_target_test( srcs = [ "ObjectInputStreamDeserialization.java", ], - expected_findings = ["java.lang.ExceptionInInitializerError"], + allowed_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh", + "java.lang.ExceptionInInitializerError", + ], target_class = "com.example.ObjectInputStreamDeserialization", ) @@ -15,7 +18,10 @@ java_fuzz_target_test( srcs = [ "ReflectiveCall.java", ], - expected_findings = ["java.lang.ExceptionInInitializerError"], + allowed_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh", + "java.lang.ExceptionInInitializerError", + ], target_class = "com.example.ReflectiveCall", ) @@ -24,25 +30,30 @@ java_fuzz_target_test( srcs = [ "LibraryLoad.java", ], + allowed_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh", + ], target_class = "com.example.LibraryLoad", # loading of native libraries is very slow on macos, # especially using Java 17 target_compatible_with = SKIP_ON_MACOS, + # The reproducer doesn't contain the sanitizer and thus runs into an ordinary ignored + # UnsatisfiedLinkError. + verify_crash_reproducer = False, ) java_fuzz_target_test( name = "ExpressionLanguageInjection", srcs = [ "ExpressionLanguageInjection.java", - "InsecureEmailValidator.java", ], + allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh"], target_class = "com.example.ExpressionLanguageInjection", + # The reproducer can't find jaz.Zer and thus doesn't crash. + verify_crash_reproducer = False, deps = [ - "@maven//:javax_el_javax_el_api", + "//sanitizers/src/test/java/com/example/el:ExpressionLanguageExample", "@maven//:javax_validation_validation_api", - "@maven//:javax_xml_bind_jaxb_api", - "@maven//:org_glassfish_javax_el", - "@maven//:org_hibernate_hibernate_validator", ], ) @@ -51,7 +62,9 @@ java_fuzz_target_test( srcs = [ "OsCommandInjectionProcessBuilder.java", ], + allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical"], target_class = "com.example.OsCommandInjectionProcessBuilder", + verify_crash_reproducer = False, ) java_fuzz_target_test( @@ -59,17 +72,22 @@ java_fuzz_target_test( srcs = [ "OsCommandInjectionRuntimeExec.java", ], + allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical"], target_class = "com.example.OsCommandInjectionRuntimeExec", + verify_crash_reproducer = False, ) java_fuzz_target_test( name = "LdapSearchInjection", srcs = [ "LdapSearchInjection.java", - "ldap/MockInitialContextFactory.java", "ldap/MockLdapContext.java", ], - expected_findings = ["javax.naming.directory.InvalidSearchFilterException"], + allowed_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical", + # The crashing input encoded by the replayer does not have valid syntax, but no hook. + "javax.naming.directory.InvalidSearchFilterException", + ], target_class = "com.example.LdapSearchInjection", deps = [ "@maven//:com_unboundid_unboundid_ldapsdk", @@ -80,10 +98,13 @@ java_fuzz_target_test( name = "LdapDnInjection", srcs = [ "LdapDnInjection.java", - "ldap/MockInitialContextFactory.java", "ldap/MockLdapContext.java", ], - expected_findings = ["javax.naming.NamingException"], + allowed_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical", + # The crashing input encoded by the reproducer does not have valid syntax, but no hook. + "javax.naming.NamingException", + ], target_class = "com.example.LdapDnInjection", deps = [ "@maven//:com_unboundid_unboundid_ldapsdk", @@ -93,7 +114,9 @@ java_fuzz_target_test( java_fuzz_target_test( name = "RegexInsecureQuoteInjection", srcs = ["RegexInsecureQuoteInjection.java"], + allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"], target_class = "com.example.RegexInsecureQuoteInjection", + verify_crash_reproducer = False, ) java_fuzz_target_test( @@ -101,7 +124,9 @@ java_fuzz_target_test( srcs = [ "RegexCanonEqInjection.java", ], + allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"], target_class = "com.example.RegexCanonEqInjection", + verify_crash_reproducer = False, ) java_fuzz_target_test( @@ -109,19 +134,40 @@ java_fuzz_target_test( srcs = [ "ClassLoaderLoadClass.java", ], - expected_findings = ["java.lang.ExceptionInInitializerError"], + allowed_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh", + # Reproducer does not find the honeypot library and doesn't have the hook. + "java.lang.ExceptionInInitializerError", + ], target_class = "com.example.ClassLoaderLoadClass", ) java_fuzz_target_test( name = "RegexRoadblocks", srcs = ["RegexRoadblocks.java"], + allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"], fuzzer_args = [ # Limit the number of runs to verify that the regex roadblocks are # cleared quickly. "-runs=22000", ], target_class = "com.example.RegexRoadblocks", + verify_crash_reproducer = False, +) + +# Catching StackOverflowErrors doesn't work reliably across all systems and JDK versions. +# It may lead to a native crash before we can handle the exception in Java, therefore the +# test is set to manual execution. +java_fuzz_target_test( + name = "StackOverflowRegexInjection", + srcs = ["StackOverflowRegexInjection.java"], + allowed_findings = ["java.util.regex.PatternSyntaxException"], + fuzzer_args = [ + "-runs=1", + ], + tags = ["manual"], + target_class = "com.example.StackOverflowRegexInjection", + verify_crash_reproducer = False, ) java_fuzz_target_test( @@ -129,7 +175,7 @@ java_fuzz_target_test( srcs = [ "SqlInjection.java", ], - expected_findings = [ + allowed_findings = [ "com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh", "org.h2.jdbc.JdbcSQLSyntaxErrorException", ], @@ -138,3 +184,90 @@ java_fuzz_target_test( "@maven//:com_h2database_h2", ], ) + +java_test( + name = "DisabledHooksTest", + size = "small", + srcs = [ + "DisabledHooksTest.java", + ], + test_class = "com.example.DisabledHooksTest", + deps = [ + "//src/main/java/com/code_intelligence/jazzer/api", + "//src/main/java/com/code_intelligence/jazzer/api:hooks", + ], +) + +java_fuzz_target_test( + name = "XPathInjection", + srcs = [ + "XPathInjection.java", + ], + allowed_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh", + ], + target_class = "com.example.XPathInjection", + # Fuzz target catches the syntax exception triggered by the reproducer without the sanitizer. + verify_crash_reproducer = False, +) + +java_fuzz_target_test( + name = "SsrfSocketConnect", + srcs = [ + "SsrfSocketConnect.java", + ], + allowed_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium", + ], + target_class = "com.example.SsrfSocketConnect", + verify_crash_reproducer = False, +) + +java_fuzz_target_test( + name = "SsrfSocketConnectToHost", + srcs = [ + "SsrfSocketConnectToHost.java", + ], + allowed_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium", + ], + target_class = "com.example.SsrfSocketConnectToHost", + verify_crash_reproducer = False, +) + +java_fuzz_target_test( + name = "SsrfUrlConnection", + srcs = [ + "SsrfUrlConnection.java", + ], + allowed_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium", + ], + target_class = "com.example.SsrfUrlConnection", + verify_crash_reproducer = False, +) + +java_fuzz_target_test( + name = "SsrfHttpClient", + srcs = [ + "SsrfHttpClient.java", + ], + allowed_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium", + ], + tags = ["no-jdk8"], + target_class = "com.example.SsrfHttpClient", + verify_crash_reproducer = False, +) + +java_fuzz_target_test( + name = "ScriptEngineInjection", + srcs = [ + "ScriptEngineInjection.java", + ], + allowed_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical", + ], + target_class = "com.example.ScriptEngineInjection", + verify_crash_reproducer = False, +) diff --git a/sanitizers/src/test/java/com/example/ClassLoaderLoadClass.java b/sanitizers/src/test/java/com/example/ClassLoaderLoadClass.java index c3fa47ac..207f29cd 100644 --- a/sanitizers/src/test/java/com/example/ClassLoaderLoadClass.java +++ b/sanitizers/src/test/java/com/example/ClassLoaderLoadClass.java @@ -22,9 +22,11 @@ public class ClassLoaderLoadClass { String input = data.consumeRemainingAsAsciiString(); try { // create an instance to trigger class initialization - ClassLoaderLoadClass.class.getClassLoader().loadClass(input).getConstructor().newInstance(); - } catch (ClassNotFoundException | InvocationTargetException | InstantiationException - | IllegalAccessException | NoSuchMethodException ignored) { + ClassLoaderLoadClass.class.getClassLoader().loadClass(input).newInstance(); + // TODO(khaled): this fails to reproduce the finding. It seems that this is related to not + // throwing a hard-to-catch error when not running in the fuzzing mode. + // ClassLoaderLoadClass.class.getClassLoader().loadClass(input).getConstructor().newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException ignored) { } } } diff --git a/sanitizers/src/test/java/com/example/DisabledHooksTest.java b/sanitizers/src/test/java/com/example/DisabledHooksTest.java new file mode 100644 index 00000000..763cd637 --- /dev/null +++ b/sanitizers/src/test/java/com/example/DisabledHooksTest.java @@ -0,0 +1,110 @@ +// 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.example; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh; +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.util.Base64; +import org.junit.After; +import org.junit.Test; + +public class DisabledHooksTest { + public static void triggerReflectiveCallSanitizer() { + try { + Class.forName("jaz.Zer").newInstance(); + } catch (ClassNotFoundException | IllegalAccessException | InstantiationException ignored) { + } + } + + public static void triggerExpressionLanguageInjectionSanitizer() throws Throwable { + try { + Class.forName("jaz.Zer").getMethod("el").invoke(null); + } catch (InvocationTargetException e) { + throw e.getCause(); + } catch (IllegalAccessException | ClassNotFoundException | NoSuchMethodException ignore) { + } + } + + public static void triggerDeserializationSanitizer() { + byte[] data = + Base64.getDecoder().decode("rO0ABXNyAAdqYXouWmVyAAAAAAAAACoCAAFCAAlzYW5pdGl6ZXJ4cAEK"); + try { + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data)); + System.out.println(ois.readObject()); + } catch (IOException | ClassNotFoundException ignore) { + } + } + + @After + public void resetDisabledHooksProperty() { + System.clearProperty("jazzer.disabled_hooks"); + } + + @Test(expected = FuzzerSecurityIssueHigh.class) + public void enableReflectiveCallSanitizer() { + triggerReflectiveCallSanitizer(); + } + + @Test(expected = FuzzerSecurityIssueHigh.class) + public void enableDeserializationSanitizer() { + triggerDeserializationSanitizer(); + } + + @Test(expected = FuzzerSecurityIssueHigh.class) + public void enableExpressionLanguageInjectionSanitizer() throws Throwable { + triggerExpressionLanguageInjectionSanitizer(); + } + + @Test + public void disableReflectiveCallSanitizer() { + System.setProperty( + "jazzer.disabled_hooks", "com.code_intelligence.jazzer.sanitizers.ReflectiveCall"); + triggerReflectiveCallSanitizer(); + } + + @Test + public void disableDeserializationSanitizer() { + System.setProperty( + "jazzer.disabled_hooks", "com.code_intelligence.jazzer.sanitizers.Deserialization"); + triggerDeserializationSanitizer(); + } + + @Test + public void disableExpressionLanguageSanitizer() throws Throwable { + System.setProperty("jazzer.disabled_hooks", + "com.code_intelligence.jazzer.sanitizers.ExpressionLanguageInjection"); + triggerExpressionLanguageInjectionSanitizer(); + } + + @Test(expected = FuzzerSecurityIssueHigh.class) + public void disableReflectiveCallAndEnableDeserialization() { + System.setProperty( + "jazzer.disabled_hooks", "com.code_intelligence.jazzer.sanitizers.ReflectiveCall"); + triggerReflectiveCallSanitizer(); + triggerDeserializationSanitizer(); + } + + @Test + public void disableAllSanitizers() throws Throwable { + System.setProperty("jazzer.disabled_hooks", + "com.code_intelligence.jazzer.sanitizers.ReflectiveCall," + + "com.code_intelligence.jazzer.sanitizers.Deserialization," + + "com.code_intelligence.jazzer.sanitizers.ExpressionLanguageInjection"); + triggerReflectiveCallSanitizer(); + triggerExpressionLanguageInjectionSanitizer(); + triggerDeserializationSanitizer(); + } +} diff --git a/sanitizers/src/test/java/com/example/ExpressionLanguageInjection.java b/sanitizers/src/test/java/com/example/ExpressionLanguageInjection.java index e26a9117..7d0192ab 100644 --- a/sanitizers/src/test/java/com/example/ExpressionLanguageInjection.java +++ b/sanitizers/src/test/java/com/example/ExpressionLanguageInjection.java @@ -15,33 +15,20 @@ package com.example; import com.code_intelligence.jazzer.api.FuzzedDataProvider; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import javax.validation.*; - -class UserData { - public UserData(String email) { - this.email = email; - } - - @ValidEmailConstraint private String email; -} - -@Constraint(validatedBy = InsecureEmailValidator.class) -@Target({ElementType.METHOD, ElementType.FIELD}) -@Retention(RetentionPolicy.RUNTIME) -@interface ValidEmailConstraint { - String message() default "Invalid email address"; - Class<?>[] groups() default {}; - Class<? extends Payload>[] payload() default {}; -} +import com.example.el.UserData; +import java.util.logging.Level; +import java.util.logging.LogManager; +import javax.validation.Validation; +import javax.validation.Validator; public class ExpressionLanguageInjection { final private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + public static void fuzzerInitialize() { + LogManager.getLogManager().getLogger("").setLevel(Level.SEVERE); + } + public static void fuzzerTestOneInput(FuzzedDataProvider data) { UserData uncheckedUserData = new UserData(data.consumeRemainingAsString()); validator.validate(uncheckedUserData); diff --git a/sanitizers/src/test/java/com/example/LdapDnInjection.java b/sanitizers/src/test/java/com/example/LdapDnInjection.java index 911db1dc..2fdf4a0c 100644 --- a/sanitizers/src/test/java/com/example/LdapDnInjection.java +++ b/sanitizers/src/test/java/com/example/LdapDnInjection.java @@ -15,20 +15,13 @@ package com.example; import com.code_intelligence.jazzer.api.FuzzedDataProvider; -import java.util.Hashtable; -import javax.naming.Context; -import javax.naming.NamingException; -import javax.naming.directory.InitialDirContext; +import com.example.ldap.MockLdapContext; +import javax.naming.directory.DirContext; import javax.naming.directory.SearchControls; +@SuppressWarnings("BanJNDI") public class LdapDnInjection { - private static InitialDirContext ctx; - - public static void fuzzerInitialize() throws NamingException { - Hashtable<String, String> env = new Hashtable<>(); - env.put(Context.INITIAL_CONTEXT_FACTORY, "com.example.ldap.MockInitialContextFactory"); - ctx = new InitialDirContext(env); - } + private static final DirContext ctx = new MockLdapContext(); public static void fuzzerTestOneInput(FuzzedDataProvider fuzzedDataProvider) throws Exception { // Externally provided DN input needs to be escaped properly diff --git a/sanitizers/src/test/java/com/example/LdapSearchInjection.java b/sanitizers/src/test/java/com/example/LdapSearchInjection.java index b3dfee74..4ac84931 100644 --- a/sanitizers/src/test/java/com/example/LdapSearchInjection.java +++ b/sanitizers/src/test/java/com/example/LdapSearchInjection.java @@ -15,20 +15,13 @@ package com.example; import com.code_intelligence.jazzer.api.FuzzedDataProvider; -import java.util.Hashtable; -import javax.naming.Context; -import javax.naming.NamingException; +import com.example.ldap.MockLdapContext; import javax.naming.directory.SearchControls; -import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.LdapContext; +@SuppressWarnings("BanJNDI") public class LdapSearchInjection { - private static InitialLdapContext ctx; - - public static void fuzzerInitialize() throws NamingException { - Hashtable<String, String> env = new Hashtable<>(); - env.put(Context.INITIAL_CONTEXT_FACTORY, "com.example.ldap.MockInitialContextFactory"); - ctx = new InitialLdapContext(env, null); - } + private static final LdapContext ctx = new MockLdapContext(); public static void fuzzerTestOneInput(FuzzedDataProvider fuzzedDataProvider) throws Exception { // Externally provided LDAP query input needs to be escaped properly diff --git a/sanitizers/src/test/java/com/example/ReflectiveCall.java b/sanitizers/src/test/java/com/example/ReflectiveCall.java index e6b62b45..d7b3e46c 100644 --- a/sanitizers/src/test/java/com/example/ReflectiveCall.java +++ b/sanitizers/src/test/java/com/example/ReflectiveCall.java @@ -22,8 +22,8 @@ public class ReflectiveCall { if (input.startsWith("@")) { String className = input.substring(1); try { - Class.forName(className); - } catch (ClassNotFoundException ignored) { + Class.forName(className).newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException ignored) { } } } diff --git a/sanitizers/src/test/java/com/example/ScriptEngineInjection.java b/sanitizers/src/test/java/com/example/ScriptEngineInjection.java new file mode 100644 index 00000000..631b7ab8 --- /dev/null +++ b/sanitizers/src/test/java/com/example/ScriptEngineInjection.java @@ -0,0 +1,171 @@ +// 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.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; +import java.util.List; +import javax.script.Bindings; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; + +public class ScriptEngineInjection { + private final static ScriptEngine engine = new DummyScriptEngine(); + private final static ScriptContext context = new DummyScriptContext(); + + private static void insecureScriptEval(String input) throws Exception { + engine.eval(new StringReader(input), context); + } + + public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Exception { + try { + insecureScriptEval(data.consumeRemainingAsAsciiString()); + } catch (Exception ignored) { + } + } + + private static class DummyScriptEngine implements ScriptEngine { + @Override + public Bindings createBindings() { + return null; + } + + @Override + public Object eval(String script) { + return null; + } + + @Override + public Object eval(Reader reader) { + return null; + } + + @Override + public Object eval(String script, ScriptContext context) { + return null; + } + + @Override + public Object eval(Reader reader, ScriptContext context) { + return null; + } + + @Override + public Object eval(String script, Bindings n) { + return null; + } + + @Override + public Object eval(Reader reader, Bindings n) { + return null; + } + + @Override + public Object get(String key) { + return null; + } + + @Override + public Bindings getBindings(int scope) { + return null; + } + + @Override + public ScriptContext getContext() { + return null; + } + + @Override + public ScriptEngineFactory getFactory() { + return null; + } + + @Override + public void put(String key, Object value) {} + + @Override + public void setBindings(Bindings bindings, int scope) {} + + @Override + public void setContext(ScriptContext context) {} + + public DummyScriptEngine() {} + } + + private static class DummyScriptContext implements ScriptContext { + @Override + public void setBindings(Bindings bindings, int scope) {} + + @Override + public Bindings getBindings(int scope) { + return null; + } + + @Override + public void setAttribute(String name, Object value, int scope) {} + + @Override + public Object getAttribute(String name, int scope) { + return null; + } + + @Override + public Object removeAttribute(String name, int scope) { + return null; + } + + @Override + public Object getAttribute(String name) { + return null; + } + + @Override + public int getAttributesScope(String name) { + return 0; + } + + @Override + public Writer getWriter() { + return null; + } + + @Override + public Writer getErrorWriter() { + return null; + } + + @Override + public void setWriter(Writer writer) {} + + @Override + public void setErrorWriter(Writer writer) {} + + @Override + public Reader getReader() { + return null; + } + + @Override + public void setReader(Reader reader) {} + + @Override + public List<Integer> getScopes() { + return null; + } + } +} diff --git a/sanitizers/src/test/java/com/example/SsrfHttpClient.java b/sanitizers/src/test/java/com/example/SsrfHttpClient.java new file mode 100644 index 00000000..6da561a9 --- /dev/null +++ b/sanitizers/src/test/java/com/example/SsrfHttpClient.java @@ -0,0 +1,39 @@ +// 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.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.io.IOException; +import java.net.*; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public class SsrfHttpClient { + private static final HttpClient CLIENT = + HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(); + + public static void fuzzerTestOneInput(FuzzedDataProvider data) + throws IOException, InterruptedException { + String hostname = data.consumeString(15); + URI uri; + try { + uri = URI.create("https://" + hostname); + HttpRequest request = HttpRequest.newBuilder().uri(uri).GET().build(); + CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IllegalArgumentException ignored) { + } + } +} diff --git a/sanitizers/src/test/java/com/example/ldap/MockInitialContextFactory.java b/sanitizers/src/test/java/com/example/SsrfSocketConnect.java index b674f5c5..f1d7a59b 100644 --- a/sanitizers/src/test/java/com/example/ldap/MockInitialContextFactory.java +++ b/sanitizers/src/test/java/com/example/SsrfSocketConnect.java @@ -1,4 +1,4 @@ -// Copyright 2021 Code Intelligence GmbH +// 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. @@ -12,15 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.example.ldap; +package com.example; -import java.util.Hashtable; -import javax.naming.Context; -import javax.naming.NamingException; -import javax.naming.spi.InitialContextFactory; +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.net.Socket; -public class MockInitialContextFactory implements InitialContextFactory { - public Context getInitialContext(Hashtable environment) { - return new MockLdapContext(); +public class SsrfSocketConnect { + public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Exception { + String hostname = data.consumeString(15); + try (Socket s = new Socket(hostname, 80)) { + s.getInetAddress(); + } } } diff --git a/sanitizers/src/test/java/com/example/SsrfSocketConnectToHost.java b/sanitizers/src/test/java/com/example/SsrfSocketConnectToHost.java new file mode 100644 index 00000000..3e60e503 --- /dev/null +++ b/sanitizers/src/test/java/com/example/SsrfSocketConnectToHost.java @@ -0,0 +1,46 @@ +// 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.example; + +import com.code_intelligence.jazzer.api.BugDetectors; +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import com.code_intelligence.jazzer.api.Jazzer; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; + +public class SsrfSocketConnectToHost { + // We don't actually care about establishing a connection and thus choose the lowest possible + // timeout. + private static final int CONNECTION_TIMEOUT_MS = 1; + + public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Exception { + String host = data.consumeAsciiString(15); + int port = data.consumeInt(1, 65535); + + try (AutoCloseable ignored = BugDetectors.allowNetworkConnections()) { + // Verify that policies nest properly. + try (AutoCloseable ignored1 = BugDetectors.allowNetworkConnections( + (String h, Integer p) -> h.equals("localhost"))) { + try (AutoCloseable ignored2 = BugDetectors.allowNetworkConnections()) { + } + try (Socket s = new Socket()) { + s.connect(new InetSocketAddress(host, port), CONNECTION_TIMEOUT_MS); + } catch (IOException ignored3) { + } + } + } + } +} diff --git a/sanitizers/src/test/java/com/example/SsrfUrlConnection.java b/sanitizers/src/test/java/com/example/SsrfUrlConnection.java new file mode 100644 index 00000000..8ea940a1 --- /dev/null +++ b/sanitizers/src/test/java/com/example/SsrfUrlConnection.java @@ -0,0 +1,33 @@ +// 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.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +public class SsrfUrlConnection { + public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Exception { + String hostname = data.consumeString(15); + try { + URL url = new URL("https://" + hostname); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("GET"); + con.getInputStream(); + } catch (IOException | IllegalArgumentException ignored) { + } + } +} diff --git a/sanitizers/src/test/java/com/example/StackOverflowRegexInjection.java b/sanitizers/src/test/java/com/example/StackOverflowRegexInjection.java new file mode 100644 index 00000000..92dfcf38 --- /dev/null +++ b/sanitizers/src/test/java/com/example/StackOverflowRegexInjection.java @@ -0,0 +1,51 @@ +// 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.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.util.regex.Pattern; + +/** + * Compiling a regex pattern can lead to stack overflows and thus is caught + * in the constructor of {@link java.util.regex.Pattern} and rethrown as a + * {@link java.util.regex.PatternSyntaxException}. + * The {@link com.code_intelligence.jazzer.sanitizers.RegexInjection} sanitizer + * uses this exception to detect injections and would incorrectly report a + * finding. Exceptions caused by stack overflows should not be handled in the + * hook as it's very unlikely that the fuzzer generates a pattern causing a + * stack overflow before it generates an invalid one. + */ +@SuppressWarnings({"ReplaceOnLiteralHasNoEffect", "ResultOfMethodCallIgnored"}) +public class StackOverflowRegexInjection { + public static void fuzzerTestOneInput(FuzzedDataProvider ignored) { + // load regex classes by using them beforehand, + // otherwise initialization would cause other issues. + Pattern.compile("\n").matcher("some string").replaceAll("\\\\n"); + + generatePatternSyntaxException(); + } + + @SuppressWarnings("InfiniteRecursion") + private static void generatePatternSyntaxException() { + // try-catch on every level to not unwind the stack + try { + // generate stack overflow + generatePatternSyntaxException(); + } catch (StackOverflowError e) { + // invoke regex injection hook + "some sting".replaceAll("\n", "\\\\n"); + } + } +} diff --git a/sanitizers/src/test/java/com/example/XPathInjection.java b/sanitizers/src/test/java/com/example/XPathInjection.java new file mode 100644 index 00000000..e8fe22a0 --- /dev/null +++ b/sanitizers/src/test/java/com/example/XPathInjection.java @@ -0,0 +1,53 @@ +// 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.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.io.*; +import javax.xml.parsers.*; +import javax.xml.xpath.*; +import org.w3c.dom.Document; +import org.xml.sax.*; + +public class XPathInjection { + static Document doc = null; + static XPath xpath = null; + + public static void fuzzerInitialize() throws Exception { + String xmlFile = "<user name=\"user\" pass=\"pass\"></user>"; + + DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance(); + domFactory.setNamespaceAware(true); + DocumentBuilder builder = domFactory.newDocumentBuilder(); + doc = builder.parse(new InputSource(new StringReader(xmlFile))); + + XPathFactory xpathFactory = XPathFactory.newInstance(); + xpath = xpathFactory.newXPath(); + } + + public static void unsafeEval(String user, String pass) { + if (user != null && pass != null) { + String expression = "/user[@name='" + user + "' and @pass='" + pass + "']"; + try { + xpath.evaluate(expression, doc, XPathConstants.BOOLEAN); + } catch (XPathExpressionException e) { + } + } + } + + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + unsafeEval(data.consumeString(20), data.consumeRemainingAsString()); + } +} diff --git a/sanitizers/src/test/java/com/example/el/BUILD.bazel b/sanitizers/src/test/java/com/example/el/BUILD.bazel new file mode 100644 index 00000000..bf12a48b --- /dev/null +++ b/sanitizers/src/test/java/com/example/el/BUILD.bazel @@ -0,0 +1,15 @@ +java_library( + name = "ExpressionLanguageExample", + srcs = [ + "InsecureEmailValidator.java", + "UserData.java", + ], + visibility = ["//sanitizers/src/test/java/com/example:__pkg__"], + deps = [ + "@maven//:javax_el_javax_el_api", + "@maven//:javax_validation_validation_api", + "@maven//:javax_xml_bind_jaxb_api", + "@maven//:org_glassfish_javax_el", + "@maven//:org_hibernate_hibernate_validator", + ], +) diff --git a/sanitizers/src/test/java/com/example/InsecureEmailValidator.java b/sanitizers/src/test/java/com/example/el/InsecureEmailValidator.java index d61e888d..e10b082e 100644 --- a/sanitizers/src/test/java/com/example/InsecureEmailValidator.java +++ b/sanitizers/src/test/java/com/example/el/InsecureEmailValidator.java @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.example; +package com.example.el; import static java.lang.String.format; diff --git a/sanitizers/src/test/java/com/example/el/UserData.java b/sanitizers/src/test/java/com/example/el/UserData.java new file mode 100644 index 00000000..305e78ee --- /dev/null +++ b/sanitizers/src/test/java/com/example/el/UserData.java @@ -0,0 +1,41 @@ +/* + * 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.example.el; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.validation.Constraint; +import javax.validation.Payload; + +public class UserData { + public UserData(String email) { + this.email = email; + } + + @ValidEmailConstraint private String email; +} + +@Constraint(validatedBy = InsecureEmailValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@interface ValidEmailConstraint { + String message() default "Invalid email address"; + Class<?>[] groups() default {}; + Class<? extends Payload>[] payload() default {}; +} |