diff options
Diffstat (limited to 'sanitizers')
31 files changed, 1785 insertions, 180 deletions
diff --git a/sanitizers/BUILD.bazel b/sanitizers/BUILD.bazel index fa84208e..fdc616a3 100644 --- a/sanitizers/BUILD.bazel +++ b/sanitizers/BUILD.bazel @@ -3,6 +3,5 @@ java_library( visibility = ["//visibility:public"], runtime_deps = [ "//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers", - "//sanitizers/src/main/java/jaz", ], ) diff --git a/sanitizers/sanitizers.bzl b/sanitizers/sanitizers.bzl index 8bdea7a9..cef4cf47 100644 --- a/sanitizers/sanitizers.bzl +++ b/sanitizers/sanitizers.bzl @@ -17,8 +17,13 @@ _sanitizer_package_prefix = "com.code_intelligence.jazzer.sanitizers." _sanitizer_class_names = [ "Deserialization", "ExpressionLanguageInjection", + "LdapInjection", "NamingContextLookup", + "OsCommandInjection", "ReflectiveCall", + "RegexInjection", + "RegexRoadblocks", + "SqlInjection", ] SANITIZER_CLASSES = [_sanitizer_package_prefix + class_name for class_name in _sanitizer_class_names] 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 65480653..1b156f9e 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,17 +1,34 @@ load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") +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", + ], +) + kt_jvm_library( name = "sanitizers", srcs = [ "Deserialization.kt", "ExpressionLanguageInjection.kt", + "LdapInjection.kt", "NamingContextLookup.kt", + "OsCommandInjection.kt", "ReflectiveCall.kt", + "RegexInjection.kt", + "SqlInjection.kt", "Utils.kt", ], visibility = ["//sanitizers:__pkg__"], + runtime_deps = [ + ":regex_roadblocks", + ], deps = [ "//agent:jazzer_api_compile_only", - "//sanitizers/src/main/java/jaz", + "@maven//:com_github_jsqlparser_jsqlparser", ], ) 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 f6401dfd..55691c1a 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 @@ -29,7 +29,7 @@ import java.util.WeakHashMap /** * Detects unsafe deserialization that leads to attacker-controlled method calls, in particular to [Object.finalize]. */ -@Suppress("unused_parameter") +@Suppress("unused_parameter", "unused") object Deserialization { private val OBJECT_INPUT_STREAM_HEADER = 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 9b1e8ca6..1dc1d5f0 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 @@ -24,7 +24,7 @@ import java.lang.invoke.MethodHandle /** * Detects injectable inputs to an expression language interpreter which may lead to remote code execution. */ -@Suppress("unused_parameter") +@Suppress("unused_parameter", "unused") object ExpressionLanguageInjection { /** @@ -44,6 +44,16 @@ object ExpressionLanguageInjection { targetClassName = "javax.el.ExpressionFactory", targetMethod = "createMethodExpression", ), + MethodHook( + type = HookType.BEFORE, + targetClassName = "jakarta.el.ExpressionFactory", + targetMethod = "createValueExpression", + ), + MethodHook( + type = HookType.BEFORE, + targetClassName = "jakarta.el.ExpressionFactory", + targetMethod = "createMethodExpression", + ), ) @JvmStatic fun hookElExpressionFactory( @@ -52,10 +62,8 @@ object ExpressionLanguageInjection { arguments: Array<Any>, hookId: Int ) { - if (arguments[1] is String) { - val expression = arguments[1] as String - Jazzer.guideTowardsContainment(expression, EXPRESSION_LANGUAGE_ATTACK, hookId) - } + val expression = arguments[1] as? String ?: return + Jazzer.guideTowardsContainment(expression, EXPRESSION_LANGUAGE_ATTACK, hookId) } // With default configurations the argument to 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 new file mode 100644 index 00000000..1afd614e --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt @@ -0,0 +1,123 @@ +// Copyright 2021 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 com.code_intelligence.jazzer.api.MethodHooks +import java.lang.Exception +import java.lang.invoke.MethodHandle +import javax.naming.NamingException +import javax.naming.directory.InvalidSearchFilterException + +/** + * Detects LDAP DN and search filter 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. + * + * Only the search methods are hooked, other methods are not used in injection attacks. Furthermore, + * only string parameters are checked, [javax.naming.Name] already validates inputs according to RFC2253. + * + * [javax.naming.directory.InitialDirContext] creates an initial context through the context factory + * stated in [javax.naming.Context.INITIAL_CONTEXT_FACTORY]. Other method calls are delegated to the + * initial context factory of type [javax.naming.directory.DirContext]. This is also the case for + * subclass [javax.naming.ldap.InitialLdapContext]. + */ +@Suppress("unused_parameter", "unused") +object LdapInjection { + + // Characters to escape in DNs + private const val NAME_CHARACTERS = "\\+<>,;\"=" + + // Characters to escape in search filter queries + private const val FILTER_CHARACTERS = "*()\\\u0000" + + @MethodHooks( + // Single object lookup, possible DN injection + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.directory.DirContext", + targetMethod = "search", + targetMethodDescriptor = "(Ljava/lang/String;Ljavax/naming.directory/Attributes;)Ljavax/naming/NamingEnumeration;", + 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"] + ), + + // Object search, possible DN and search filter injection + MethodHook( + type = HookType.REPLACE, + 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"] + ), + 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"] + ), + 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"] + ) + ) + @JvmStatic + fun searchLdapContext(method: MethodHandle, thisObject: Any?, args: Array<Any>, hookId: Int): Any? { + try { + return method.invokeWithArguments(thisObject, *args).also { + (args[0] as? String)?.let { name -> + Jazzer.guideTowardsEquality(name, NAME_CHARACTERS, hookId) + } + (args[1] as? String)?.let { filter -> + Jazzer.guideTowardsEquality(filter, FILTER_CHARACTERS, 31 * hookId) + } + } + } catch (e: Exception) { + when (e) { + is InvalidSearchFilterException -> + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueCritical( + """LDAP Injection +Search filters based on untrusted data must be escape 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.""" + ) + ) + } + 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 2d4fb9cf..56e12f03 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 @@ -22,6 +22,7 @@ import com.code_intelligence.jazzer.api.MethodHooks import java.lang.invoke.MethodHandle import javax.naming.CommunicationException +@Suppress("unused") object NamingContextLookup { // The particular URL g.co is used here since it is: @@ -31,6 +32,7 @@ object NamingContextLookup { private const val LDAP_MARKER = "ldap://g.co/" private const val RMI_MARKER = "rmi://g.co/" + @Suppress("UNUSED_PARAMETER") @MethodHooks( MethodHook( type = HookType.REPLACE, @@ -40,46 +42,10 @@ object NamingContextLookup { ), MethodHook( type = HookType.REPLACE, - targetClassName = "javax.naming.InitialContext", - targetMethod = "lookup", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.naming.InitialDirContext", - targetMethod = "lookup", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.naming.InitialLdapContext", - targetMethod = "lookup", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), - MethodHook( - type = HookType.REPLACE, targetClassName = "javax.naming.Context", targetMethod = "lookupLink", targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", ), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.naming.InitialContext", - targetMethod = "lookupLink", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.naming.InitialDirContext", - targetMethod = "lookupLink", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.naming.InitialLdapContext", - targetMethod = "lookupLink", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), ) @JvmStatic fun lookupHook(method: MethodHandle?, thisObject: Any?, args: Array<Any?>, hookId: Int): Any { 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 new file mode 100644 index 00000000..d3adc207 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt @@ -0,0 +1,61 @@ +// Copyright 2021 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.lang.invoke.MethodHandle + +/** + * Detects unsafe execution of OS commands using [ProcessBuilder]. + * Executing OS commands based on attacker-controlled data could lead to arbitrary could execution. + * + * All public methods providing the command to execute end up in [java.lang.ProcessImpl.start], + * so calls to this method are hooked. + * Only the first entry of the given command array is analyzed. It states the executable and must + * not include attacker provided data. + */ +@Suppress("unused_parameter", "unused") +object OsCommandInjection { + + // Short and probably non-existing command name + private const val COMMAND = "jazze" + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.lang.ProcessImpl", + targetMethod = "start", + additionalClassesToHook = ["java.lang.ProcessBuilder"] + ) + @JvmStatic + fun processImplStartHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + // Calling ProcessBuilder already checks if command array is empty + @Suppress("UNCHECKED_CAST") + (args[0] as? Array<String>)?.first().let { cmd -> + if (cmd == COMMAND) { + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueCritical( + """OS Command Injection +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 7842d879..0fcabe36 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 @@ -14,21 +14,59 @@ 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 /** - * Detects unsafe reflective calls that lead to attacker-controlled method calls. + * Detects unsafe calls that lead to attacker-controlled class loading. + * + * Guide the fuzzer to load honeypot class via [Class.forName] or [ClassLoader.loadClass]. */ -@Suppress("unused_parameter") +@Suppress("unused_parameter", "unused") object ReflectiveCall { - @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Class", targetMethod = "forName") + @MethodHooks( + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Class", targetMethod = "forName", targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Class;"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Class", targetMethod = "forName", targetMethodDescriptor = "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.ClassLoader", targetMethod = "loadClass", targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Class;"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.ClassLoader", targetMethod = "loadClass", targetMethodDescriptor = "(Ljava/lang/String;Z)Ljava/lang/Class;"), + ) @JvmStatic - fun classForNameHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + fun loadClassHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { val className = args[0] as? String ?: return Jazzer.guideTowardsEquality(className, HONEYPOT_CLASS_NAME, hookId) } + + @MethodHooks( + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Class", targetMethod = "forName", targetMethodDescriptor = "(Ljava/lang/Module;Ljava/lang/String;)Ljava/lang/Class;"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.ClassLoader", targetMethod = "loadClass", targetMethodDescriptor = "(Ljava/lang/Module;Ljava/lang/String;)Ljava/lang/Class;"), + ) + @JvmStatic + fun loadClassWithModuleHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + val className = args[1] as? String ?: return + Jazzer.guideTowardsEquality(className, HONEYPOT_CLASS_NAME, hookId) + } + + @MethodHooks( + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Runtime", targetMethod = "load"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Runtime", targetMethod = "loadLibrary"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System", targetMethod = "load"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System", targetMethod = "loadLibrary"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System", targetMethod = "mapLibraryName"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.ClassLoader", targetMethod = "findLibrary"), + ) + @JvmStatic + fun loadLibraryHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + val libraryName = args[0] as? String ?: return + if (libraryName == HONEYPOT_LIBRARY_NAME) { + Jazzer.reportFindingFromHook( + 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 new file mode 100644 index 00000000..def5f6e3 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt @@ -0,0 +1,160 @@ +// 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.FuzzerSecurityIssueLow +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 java.util.regex.Pattern +import java.util.regex.PatternSyntaxException + +@Suppress("unused_parameter", "unused") +object RegexInjection { + /** + * Part of an OOM "exploit" for [java.util.regex.Pattern.compile] with the + * [java.util.regex.Pattern.CANON_EQ] flag, formed by three consecutive combining marks, in this + * case grave accents: ◌̀. + * See [compileWithFlagsHook] for details. + */ + private const val CANON_EQ_ALMOST_EXPLOIT = "\u0300\u0300\u0300" + + /** + * When injected into a regex pattern, helps the fuzzer break out of quotes and character + * classes in order to cause a [PatternSyntaxException]. + */ + private const val FORCE_PATTERN_SYNTAX_EXCEPTION_PATTERN = "\\E]\\E]]]]]]" + + @MethodHook( + type = HookType.REPLACE, + targetClassName = "java.util.regex.Pattern", + targetMethod = "compile", + targetMethodDescriptor = "(Ljava/lang/String;I)Ljava/util/regex/Pattern;" + ) + @JvmStatic + fun compileWithFlagsHook(method: MethodHandle, alwaysNull: Any?, args: Array<Any?>, hookId: Int): Any? { + val pattern = args[0] as String? + val hasCanonEqFlag = ((args[1] as Int) and Pattern.CANON_EQ) != 0 + return hookInternal(method, pattern, hasCanonEqFlag, hookId, *args) + } + + @MethodHooks( + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.util.regex.Pattern", + targetMethod = "compile", + 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" + ), + ) + @JvmStatic + fun patternHook(method: MethodHandle, alwaysNull: Any?, args: Array<Any?>, hookId: Int): Any? { + return hookInternal(method, args[0] as String?, false, hookId, *args) + } + + @MethodHooks( + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.lang.String", + targetMethod = "matches", + targetMethodDescriptor = "(Ljava/lang/String;)Z", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.lang.String", + targetMethod = "replaceAll", + targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.lang.String", + targetMethod = "replaceFirst", + targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.lang.String", + targetMethod = "split", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/String;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.lang.String", + targetMethod = "split", + targetMethodDescriptor = "(Ljava/lang/String;I)Ljava/lang/String;", + ), + ) + @JvmStatic + fun stringHook(method: MethodHandle, thisObject: Any?, args: Array<Any?>, hookId: Int): Any? { + return hookInternal(method, args[0] as String?, false, hookId, thisObject, *args) + } + + private fun hookInternal( + method: MethodHandle, + pattern: String?, + hasCanonEqFlag: Boolean, + hookId: Int, + vararg args: Any? + ): Any? { + if (hasCanonEqFlag && pattern != null) { + // With CANON_EQ enabled, Pattern.compile allocates an array with a size that is + // (super-)exponential in the number of consecutive Unicode combining marks. We use a mild case + // of this as a magic string based on which we trigger a finding. + // Note: The fuzzer might trigger an OutOfMemoryError or NegativeArraySizeException (if the size + // of the array overflows an int) by chance before it correctly emits this "exploit". In that + // case, we report the original exception instead. + if (pattern.contains(CANON_EQ_ALMOST_EXPLOIT)) { + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueLow( + """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(...).""" + ) + ) + } else { + Jazzer.guideTowardsContainment(pattern, CANON_EQ_ALMOST_EXPLOIT, hookId) + } + } + try { + return method.invokeWithArguments(*args).also { + // Only submit a fuzzer hint if no exception has been thrown. + if (!hasCanonEqFlag && pattern != null) { + Jazzer.guideTowardsContainment(pattern, FORCE_PATTERN_SYNTAX_EXCEPTION_PATTERN, hookId) + } + } + } catch (e: Exception) { + if (e is PatternSyntaxException) { + 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 + ) + ) + } + 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 new file mode 100644 index 00000000..1043ac02 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java @@ -0,0 +1,322 @@ +// 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 com.code_intelligence.jazzer.sanitizers.utils.ReflectionUtils.INVALID_OFFSET; +import static com.code_intelligence.jazzer.sanitizers.utils.ReflectionUtils.field; +import static com.code_intelligence.jazzer.sanitizers.utils.ReflectionUtils.nestedClass; +import static com.code_intelligence.jazzer.sanitizers.utils.ReflectionUtils.offset; + +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 java.lang.invoke.MethodHandle; +import java.util.WeakHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import sun.misc.Unsafe; + +/** + * The hooks in this class extend the reach of Jazzer's string compare instrumentation to literals + * (both strings and characters) that are part of regular expression patterns. + * <p> + * Internally, the Java standard library represents a compiled regular expression as a graph of + * instances of Pattern$Node instances, each of which represents a single unit of the full + * expression and provides a `match` function that takes a {@link Matcher}, a {@link CharSequence} + * to match against and an index into the sequence. With a hook on this method for every subclass of + * Pattern$Node, the contents of the node can be inspected and an appropriate string comparison + * between the relevant part of the input string and the literal string can be reported. + */ +public final class RegexRoadblocks { + // The number of characters preceding one that failed a character predicate to include in the + // reported string comparison. + private static final int CHARACTER_COMPARE_CONTEXT_LENGTH = 10; + + private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe(); + private static final Class<?> SLICE_NODE = nestedClass(Pattern.class, "SliceNode"); + private static final long SLICE_NODE_BUFFER_OFFSET = + offset(UNSAFE, field(SLICE_NODE, "buffer", int[].class)); + private static final Class<?> CHAR_PREDICATE = nestedClass(Pattern.class, "CharPredicate"); + private static final Class<?> CHAR_PROPERTY = nestedClass(Pattern.class, "CharProperty"); + private static final long CHAR_PROPERTY_PREDICATE_OFFSET = offset( + UNSAFE, field(CHAR_PROPERTY, "predicate", nestedClass(Pattern.class, "CharPredicate"))); + private static final Class<?> BIT_CLASS = nestedClass(Pattern.class, "BitClass"); + private static final long BIT_CLASS_BITS_OFFSET = + offset(UNSAFE, field(BIT_CLASS, "bits", boolean[].class)); + + // Weakly map CharPredicate instances to characters that satisfy the predicate. Since + // CharPredicate instances are usually lambdas, we collect their solutions by hooking the + // functions constructing them rather than extracting the solutions via reflection. + // Note: Java 8 uses anonymous subclasses of CharProperty instead of lambdas implementing + // CharPredicate, hence CharProperty instances are used as keys instead in that case. + 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", + additionalClassesToHook = + { + "java.util.regex.Matcher", + "java.util.regex.Pattern$BackRef", + "java.util.regex.Pattern$Behind", + "java.util.regex.Pattern$BehindS", + "java.util.regex.Pattern$BmpCharProperty", + "java.util.regex.Pattern$BmpCharPropertyGreedy", + "java.util.regex.Pattern$BnM", + "java.util.regex.Pattern$BnMS", + "java.util.regex.Pattern$Bound", + "java.util.regex.Pattern$Branch", + "java.util.regex.Pattern$BranchConn", + "java.util.regex.Pattern$CharProperty", + "java.util.regex.Pattern$CharPropertyGreedy", + "java.util.regex.Pattern$CIBackRef", + "java.util.regex.Pattern$Caret", + "java.util.regex.Pattern$Curly", + "java.util.regex.Pattern$Conditional", + "java.util.regex.Pattern$First", + "java.util.regex.Pattern$GraphemeBound", + "java.util.regex.Pattern$GroupCurly", + "java.util.regex.Pattern$GroupHead", + "java.util.regex.Pattern$GroupRef", + "java.util.regex.Pattern$LastMatch", + "java.util.regex.Pattern$LazyLoop", + "java.util.regex.Pattern$LineEnding", + "java.util.regex.Pattern$Loop", + "java.util.regex.Pattern$Neg", + "java.util.regex.Pattern$NFCCharProperty", + "java.util.regex.Pattern$NotBehind", + "java.util.regex.Pattern$NotBehindS", + "java.util.regex.Pattern$Pos", + "java.util.regex.Pattern$Ques", + "java.util.regex.Pattern$Slice", + "java.util.regex.Pattern$SliceI", + "java.util.regex.Pattern$SliceIS", + "java.util.regex.Pattern$SliceS", + "java.util.regex.Pattern$SliceU", + "java.util.regex.Pattern$Start", + "java.util.regex.Pattern$StartS", + "java.util.regex.Pattern$UnixCaret", + "java.util.regex.Pattern$UnixDollar", + "java.util.regex.Pattern$XGrapheme", + }) + public static void + nodeMatchHook(MethodHandle method, Object node, Object[] args, int hookId, Boolean matched) { + if (HOOK_DISABLED || matched || node == null) + return; + Matcher matcher = (Matcher) args[0]; + if (matcher == null) + return; + int i = (int) args[1]; + CharSequence seq = (CharSequence) args[2]; + if (seq == null) + return; + + if (SLICE_NODE != null && SLICE_NODE.isInstance(node)) { + // The node encodes a match against a fixed string literal. Extract the literal and report a + // comparison between it and the subsequence of seq starting at i. + if (SLICE_NODE_BUFFER_OFFSET == INVALID_OFFSET) + return; + int currentLength = limitedLength(matcher.regionEnd() - i); + String current = seq.subSequence(i, i + currentLength).toString(); + + // All the subclasses of SliceNode store the literal in an int[], which we have to truncate to + // a char[]. + int[] buffer = (int[]) UNSAFE.getObject(node, SLICE_NODE_BUFFER_OFFSET); + char[] charBuffer = new char[limitedLength(buffer.length)]; + for (int j = 0; j < charBuffer.length; j++) { + charBuffer[j] = (char) buffer[j]; + } + String target = new String(charBuffer); + + Jazzer.guideTowardsEquality(current, target, perRegexId(hookId, matcher)); + } else if (CHAR_PROPERTY != null && CHAR_PROPERTY.isInstance(node)) { + // The node encodes a match against a class of characters, which may be hard to guess unicode + // characters. We rely on further hooks to track the relation between these nodes and + // characters satisfying their match function since the nodes themselves encode this + // information in lambdas, which are difficult to dissect via reflection. If we know a + // matching character, report a one-character (plus context) string comparison. + Object solutionKey; + if (CHAR_PROPERTY_PREDICATE_OFFSET == INVALID_OFFSET) { + if (CHAR_PREDICATE == null) { + // We are likely running against JDK 8, which directly construct subclasses of + // CharProperty rather than using lambdas implementing CharPredicate. + solutionKey = node; + } else { + return; + } + } else { + solutionKey = UNSAFE.getObject(node, CHAR_PROPERTY_PREDICATE_OFFSET); + } + if (solutionKey == null) + return; + Character solution = predicateSolution(solutionKey); + if (solution == null) + return; + // We report a string comparison rather than an integer comparison for two reasons: + // 1. If the characters are four byte codepoints, they will be coded on six bytes (a surrogate + // pair) in CESU-8, which is the encoding assumed for the fuzzer input, whereas ASCII + // characters will be coded on a single byte. By using the string compare hook, we do not + // have to worry about the encoding at this point. + // 2. The same character can appear multiple times in both the pattern and the matched string, + // which makes it harder for the fuzzer to determine the correct position to mutate the + // current character into the matching character. By providing a short section of the + // input string preceding the incorrect character, we increase the chance of a hit. + String context = + seq.subSequence(Math.max(0, i - CHARACTER_COMPARE_CONTEXT_LENGTH), i).toString(); + String current = seq.subSequence(i, Math.min(i + 1, matcher.regionEnd())).toString(); + String target = Character.toString(solution); + Jazzer.guideTowardsEquality(context + current, context + target, perRegexId(hookId, matcher)); + } + } + + // This and all following hooks track the relation between a CharPredicate or CharProperty + // instance and a character that matches it. We use an after hook on the factory methods so that + // we have access to the parameters and the created instance at the same time. + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "Single", + targetMethodDescriptor = "(I)Ljava/util/regex/Pattern$BmpCharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "SingleI", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "SingleS", + targetMethodDescriptor = "(I)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "SingleU", + targetMethodDescriptor = "(I)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + public static void + singleHook(MethodHandle method, Object node, Object[] args, int hookId, Object predicate) { + if (HOOK_DISABLED || predicate == null) + return; + PREDICATE_SOLUTIONS.get().put(predicate, (char) (int) args[0]); + } + + // Java 8 uses classes extending CharProperty instead of lambdas implementing CharPredicate to + // match single characters, so also hook those. + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$Single", + targetMethod = "<init>", additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$SingleI", + targetMethod = "<init>", additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$SingleS", + targetMethod = "<init>", additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$SingleU", + targetMethod = "<init>", additionalClassesToHook = {"java.util.regex.Pattern"}) + public static void + java8SingleHook( + MethodHandle method, Object property, Object[] args, int hookId, Object alwaysNull) { + if (HOOK_DISABLED || property == null) + return; + PREDICATE_SOLUTIONS.get().put(property, (char) (int) args[0]); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "Range", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "CIRange", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "CIRangeU", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + // Java 8 uses anonymous classes extending CharProperty instead of lambdas implementing + // CharPredicate to match single characters, so also hook those. + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "rangeFor", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharProperty;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "caseInsensitiveRangeFor", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharProperty;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + public static void + rangeHook(MethodHandle method, Object node, Object[] args, int hookId, Object predicate) { + if (HOOK_DISABLED || predicate == null) + return; + PREDICATE_SOLUTIONS.get().put(predicate, (char) (int) args[0]); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$CharPredicate", + targetMethod = "union", + targetMethodDescriptor = + "(Ljava/util/regex/Pattern$CharPredicate;)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + // Java 8 uses anonymous classes extending CharProperty instead of lambdas implementing + // CharPredicate to match single characters, so also hook union for those. Even though the classes + // of the parameters will be different, the actual implementation of the hook is the same in this + // case. + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "union", + targetMethodDescriptor = + "(Ljava/util/regex/Pattern$CharProperty;Ljava/util/regex/Pattern$CharProperty;)Ljava/util/regex/Pattern$CharProperty;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + public static void + unionHook( + MethodHandle method, Object thisObject, Object[] args, int hookId, Object unionPredicate) { + if (HOOK_DISABLED || unionPredicate == null) + return; + Character solution = predicateSolution(thisObject); + if (solution == null) + solution = predicateSolution(args[0]); + if (solution == null) + return; + PREDICATE_SOLUTIONS.get().put(unionPredicate, solution); + } + + private static Character predicateSolution(Object charPredicate) { + return PREDICATE_SOLUTIONS.get().computeIfAbsent(charPredicate, unused -> { + if (BIT_CLASS != null && BIT_CLASS.isInstance(charPredicate)) { + // BitClass instances have an empty bits array at construction time, so we scan their + // constants lazily when needed. + 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; + } + } + } + return null; + }); + } + + // Limits a length to the maximum length libFuzzer will read up to in a callback. + private static int limitedLength(int length) { + return Math.min(length, 64); + } + + // hookId only takes one distinct value per Node subclass. In order to get different regex matches + // to be tracked similar to different instances of string compares, we mix in the hash of the + // underlying pattern. We expect patterns to be static almost always, so that this should not fill + // up the value profile map too quickly. + private static int perRegexId(int hookId, Matcher matcher) { + return hookId ^ matcher.pattern().toString().hashCode(); + } +} 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 new file mode 100644 index 00000000..f317bcc8 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.kt @@ -0,0 +1,113 @@ +// 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/Utils.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Utils.kt index 3166773b..219490d8 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Utils.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Utils.kt @@ -21,6 +21,7 @@ import java.io.InputStream * jaz.Zer is a honeypot class: All of its methods report a finding when called. */ const val HONEYPOT_CLASS_NAME = "jaz.Zer" +const val HONEYPOT_LIBRARY_NAME = "jazzer_honeypot" internal fun Short.toBytes(): ByteArray { return byteArrayOf( @@ -43,9 +44,20 @@ internal fun ByteArray.indexOf(needle: ByteArray): Int { } internal fun guideMarkableInputStreamTowardsEquality(stream: InputStream, target: ByteArray, id: Int) { + fun readBytes(stream: InputStream, size: Int): ByteArray { + val current = ByteArray(size) + var n = 0 + while (n < size) { + val count = stream.read(current, n, size - n) + if (count < 0) break + n += count + } + return current + } + check(stream.markSupported()) stream.mark(target.size) - val current = stream.readNBytes(target.size) + val current = readBytes(stream, target.size) stream.reset() Jazzer.guideTowardsEquality(current, target, id) } diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/BUILD.bazel b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/BUILD.bazel new file mode 100644 index 00000000..c7258447 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/BUILD.bazel @@ -0,0 +1,7 @@ +java_library( + name = "reflection_utils", + srcs = ["ReflectionUtils.java"], + visibility = [ + "//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers:__pkg__", + ], +) diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/ReflectionUtils.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/ReflectionUtils.java new file mode 100644 index 00000000..fd6ac72f --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/ReflectionUtils.java @@ -0,0 +1,62 @@ +// 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.utils; + +import java.lang.reflect.Field; +import sun.misc.Unsafe; + +public final class ReflectionUtils { + public static final long INVALID_OFFSET = Long.MIN_VALUE; + + private static final boolean JAZZER_REFLECTION_DEBUG = + "1".equals(System.getenv("JAZZER_REFLECTION_DEBUG")); + + public static Class<?> clazz(String className) { + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + if (JAZZER_REFLECTION_DEBUG) + e.printStackTrace(); + return null; + } + } + + public static Class<?> nestedClass(Class<?> parentClass, String nestedClassName) { + return clazz(parentClass.getName() + "$" + nestedClassName); + } + + public static Field field(Class<?> clazz, String name, Class<?> type) { + if (clazz == null) + return null; + try { + Field field = clazz.getDeclaredField(name); + if (!field.getType().equals(type)) { + throw new NoSuchFieldException( + "Expected " + name + " to be of type " + type + " (is: " + field.getType() + ")"); + } + return field; + } catch (NoSuchFieldException e) { + if (JAZZER_REFLECTION_DEBUG) + e.printStackTrace(); + return null; + } + } + + public static long offset(Unsafe unsafe, Field field) { + if (unsafe == null || field == null) + return INVALID_OFFSET; + return unsafe.objectFieldOffset(field); + } +} diff --git a/sanitizers/src/main/java/jaz/BUILD.bazel b/sanitizers/src/main/java/jaz/BUILD.bazel deleted file mode 100644 index 81275a31..00000000 --- a/sanitizers/src/main/java/jaz/BUILD.bazel +++ /dev/null @@ -1,12 +0,0 @@ -java_library( - name = "jaz", - srcs = [ - "Ter.java", - "Zer.java", - ], - visibility = [ - "//sanitizers:__pkg__", - "//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers:__pkg__", - ], - deps = ["//agent:jazzer_api_compile_only"], -) diff --git a/sanitizers/src/main/java/jaz/Zer.java b/sanitizers/src/main/java/jaz/Zer.java deleted file mode 100644 index 0b27609c..00000000 --- a/sanitizers/src/main/java/jaz/Zer.java +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2021 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 jaz; - -import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh; -import com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium; -import com.code_intelligence.jazzer.api.Jazzer; -import java.io.IOException; -import java.io.ObjectInputStream; - -/** - * A honeypot class that reports an appropriate finding on any interaction with one of its methods - * or initializers. - * - * Note: This class must not be referenced in any way by the rest of the code, not even statically. - * When referring to it, always use its hardcoded class name "jaz.Zer". - */ -@SuppressWarnings("unused") -public class Zer implements java.io.Serializable { - static final long serialVersionUID = 42L; - - private static final Throwable staticInitializerCause; - - static { - staticInitializerCause = new FuzzerSecurityIssueMedium("finalize call on arbitrary object"); - } - - public Zer() { - Jazzer.reportFindingFromHook( - new FuzzerSecurityIssueMedium("default constructor call on arbitrary object")); - } - - public Zer(String arg1) { - Jazzer.reportFindingFromHook( - new FuzzerSecurityIssueMedium("String constructor call on arbitrary object")); - } - - public Zer(String arg1, Throwable arg2) { - Jazzer.reportFindingFromHook( - new FuzzerSecurityIssueMedium("(String, Throwable) constructor call on arbitrary object")); - } - - private String jaz; - - public String getJaz() { - Jazzer.reportFindingFromHook(new FuzzerSecurityIssueMedium("getter call on arbitrary object")); - return jaz; - } - - public void setJaz(String jaz) { - Jazzer.reportFindingFromHook(new FuzzerSecurityIssueMedium("setter call on arbitrary object")); - this.jaz = jaz; - } - - @Override - public int hashCode() { - Jazzer.reportFindingFromHook( - new FuzzerSecurityIssueMedium("hashCode call on arbitrary object")); - return super.hashCode(); - } - - @Override - public boolean equals(Object obj) { - Jazzer.reportFindingFromHook(new FuzzerSecurityIssueMedium("equals call on arbitrary object")); - return super.equals(obj); - } - - @Override - protected Object clone() throws CloneNotSupportedException { - Jazzer.reportFindingFromHook(new FuzzerSecurityIssueMedium("clone call on arbitrary object")); - return super.clone(); - } - - @Override - public String toString() { - Jazzer.reportFindingFromHook( - new FuzzerSecurityIssueMedium("toString call on arbitrary object")); - return super.toString(); - } - - @Override - protected void finalize() throws Throwable { - // finalize is invoked automatically by the GC with an uninformative stack trace. We use the - // stack trace prerecorded in the static initializer. - Jazzer.reportFindingFromHook(staticInitializerCause); - super.finalize(); - } - - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - Jazzer.reportFindingFromHook(new FuzzerSecurityIssueHigh("Remote Code Execution\n" - + " Deserialization of arbitrary classes with custom readObject may allow remote\n" - + " code execution depending on the classpath.")); - in.defaultReadObject(); - } -} diff --git a/sanitizers/src/test/java/com/example/BUILD.bazel b/sanitizers/src/test/java/com/example/BUILD.bazel index d148545a..5d2e1ca5 100644 --- a/sanitizers/src/test/java/com/example/BUILD.bazel +++ b/sanitizers/src/test/java/com/example/BUILD.bazel @@ -1,10 +1,12 @@ load("//bazel:fuzz_target.bzl", "java_fuzz_target_test") +load("//bazel:compat.bzl", "SKIP_ON_MACOS") java_fuzz_target_test( name = "ObjectInputStreamDeserialization", srcs = [ "ObjectInputStreamDeserialization.java", ], + expected_findings = ["java.lang.ExceptionInInitializerError"], target_class = "com.example.ObjectInputStreamDeserialization", ) @@ -13,10 +15,22 @@ java_fuzz_target_test( srcs = [ "ReflectiveCall.java", ], + expected_findings = ["java.lang.ExceptionInInitializerError"], target_class = "com.example.ReflectiveCall", ) java_fuzz_target_test( + name = "LibraryLoad", + srcs = [ + "LibraryLoad.java", + ], + target_class = "com.example.LibraryLoad", + # loading of native libraries is very slow on macos, + # especially using Java 17 + target_compatible_with = SKIP_ON_MACOS, +) + +java_fuzz_target_test( name = "ExpressionLanguageInjection", srcs = [ "ExpressionLanguageInjection.java", @@ -27,6 +41,100 @@ java_fuzz_target_test( "@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", ], ) + +java_fuzz_target_test( + name = "OsCommandInjectionProcessBuilder", + srcs = [ + "OsCommandInjectionProcessBuilder.java", + ], + target_class = "com.example.OsCommandInjectionProcessBuilder", +) + +java_fuzz_target_test( + name = "OsCommandInjectionRuntimeExec", + srcs = [ + "OsCommandInjectionRuntimeExec.java", + ], + target_class = "com.example.OsCommandInjectionRuntimeExec", +) + +java_fuzz_target_test( + name = "LdapSearchInjection", + srcs = [ + "LdapSearchInjection.java", + "ldap/MockInitialContextFactory.java", + "ldap/MockLdapContext.java", + ], + expected_findings = ["javax.naming.directory.InvalidSearchFilterException"], + target_class = "com.example.LdapSearchInjection", + deps = [ + "@maven//:com_unboundid_unboundid_ldapsdk", + ], +) + +java_fuzz_target_test( + name = "LdapDnInjection", + srcs = [ + "LdapDnInjection.java", + "ldap/MockInitialContextFactory.java", + "ldap/MockLdapContext.java", + ], + expected_findings = ["javax.naming.NamingException"], + target_class = "com.example.LdapDnInjection", + deps = [ + "@maven//:com_unboundid_unboundid_ldapsdk", + ], +) + +java_fuzz_target_test( + name = "RegexInsecureQuoteInjection", + srcs = ["RegexInsecureQuoteInjection.java"], + target_class = "com.example.RegexInsecureQuoteInjection", +) + +java_fuzz_target_test( + name = "RegexCanonEqInjection", + srcs = [ + "RegexCanonEqInjection.java", + ], + target_class = "com.example.RegexCanonEqInjection", +) + +java_fuzz_target_test( + name = "ClassLoaderLoadClass", + srcs = [ + "ClassLoaderLoadClass.java", + ], + expected_findings = ["java.lang.ExceptionInInitializerError"], + target_class = "com.example.ClassLoaderLoadClass", +) + +java_fuzz_target_test( + name = "RegexRoadblocks", + srcs = ["RegexRoadblocks.java"], + fuzzer_args = [ + # Limit the number of runs to verify that the regex roadblocks are + # cleared quickly. + "-runs=22000", + ], + target_class = "com.example.RegexRoadblocks", +) + +java_fuzz_target_test( + name = "SqlInjection", + srcs = [ + "SqlInjection.java", + ], + expected_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh", + "org.h2.jdbc.JdbcSQLSyntaxErrorException", + ], + target_class = "com.example.SqlInjection", + deps = [ + "@maven//:com_h2database_h2", + ], +) diff --git a/sanitizers/src/test/java/com/example/ClassLoaderLoadClass.java b/sanitizers/src/test/java/com/example/ClassLoaderLoadClass.java new file mode 100644 index 00000000..c3fa47ac --- /dev/null +++ b/sanitizers/src/test/java/com/example/ClassLoaderLoadClass.java @@ -0,0 +1,30 @@ +// Copyright 2021 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.lang.reflect.InvocationTargetException; + +public class ClassLoaderLoadClass { + public static void fuzzerTestOneInput(FuzzedDataProvider data) throws InterruptedException { + 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) { + } + } +} diff --git a/sanitizers/src/test/java/com/example/LdapDnInjection.java b/sanitizers/src/test/java/com/example/LdapDnInjection.java new file mode 100644 index 00000000..911db1dc --- /dev/null +++ b/sanitizers/src/test/java/com/example/LdapDnInjection.java @@ -0,0 +1,39 @@ +// Copyright 2021 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.Hashtable; +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchControls; + +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); + } + + public static void fuzzerTestOneInput(FuzzedDataProvider fuzzedDataProvider) throws Exception { + // Externally provided DN input needs to be escaped properly + String ou = fuzzedDataProvider.consumeRemainingAsString(); + String base = "ou=" + ou + ",dc=example,dc=com"; + ctx.search(base, "(&(uid=foo)(cn=bar))", new SearchControls()); + } +} diff --git a/sanitizers/src/test/java/com/example/LdapSearchInjection.java b/sanitizers/src/test/java/com/example/LdapSearchInjection.java new file mode 100644 index 00000000..b3dfee74 --- /dev/null +++ b/sanitizers/src/test/java/com/example/LdapSearchInjection.java @@ -0,0 +1,39 @@ +// Copyright 2021 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.Hashtable; +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.SearchControls; +import javax.naming.ldap.InitialLdapContext; + +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); + } + + public static void fuzzerTestOneInput(FuzzedDataProvider fuzzedDataProvider) throws Exception { + // Externally provided LDAP query input needs to be escaped properly + String username = fuzzedDataProvider.consumeRemainingAsAsciiString(); + String filter = "(&(uid=" + username + ")(ou=security))"; + ctx.search("dc=example,dc=com", filter, new SearchControls()); + } +} diff --git a/sanitizers/src/test/java/com/example/LibraryLoad.java b/sanitizers/src/test/java/com/example/LibraryLoad.java new file mode 100644 index 00000000..81411767 --- /dev/null +++ b/sanitizers/src/test/java/com/example/LibraryLoad.java @@ -0,0 +1,29 @@ +// Copyright 2021 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; + +public class LibraryLoad { + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + String input = data.consumeRemainingAsAsciiString(); + + try { + System.loadLibrary(input); + } catch (SecurityException | UnsatisfiedLinkError | NullPointerException + | IllegalArgumentException ignored) { + } + } +} diff --git a/sanitizers/src/test/java/com/example/OsCommandInjectionProcessBuilder.java b/sanitizers/src/test/java/com/example/OsCommandInjectionProcessBuilder.java new file mode 100644 index 00000000..f5d52782 --- /dev/null +++ b/sanitizers/src/test/java/com/example/OsCommandInjectionProcessBuilder.java @@ -0,0 +1,35 @@ +// Copyright 2021 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.concurrent.TimeUnit; + +public class OsCommandInjectionProcessBuilder { + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + String input = data.consumeRemainingAsAsciiString(); + try { + ProcessBuilder processBuilder = new ProcessBuilder(input); + processBuilder.environment().clear(); + Process process = processBuilder.start(); + // This should be way faster, but we have to wait until the call is done + if (!process.waitFor(10, TimeUnit.MILLISECONDS)) { + process.destroyForcibly(); + } + } catch (Exception ignored) { + // Ignore execution and setup exceptions + } + } +} diff --git a/sanitizers/src/test/java/com/example/OsCommandInjectionRuntimeExec.java b/sanitizers/src/test/java/com/example/OsCommandInjectionRuntimeExec.java new file mode 100644 index 00000000..c620a751 --- /dev/null +++ b/sanitizers/src/test/java/com/example/OsCommandInjectionRuntimeExec.java @@ -0,0 +1,35 @@ +// Copyright 2021 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 static java.lang.Runtime.getRuntime; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.util.concurrent.TimeUnit; + +public class OsCommandInjectionRuntimeExec { + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + String input = data.consumeRemainingAsAsciiString(); + try { + Process process = getRuntime().exec(input, new String[] {}); + // This should be way faster, but we have to wait until the call is done + if (!process.waitFor(10, TimeUnit.MILLISECONDS)) { + process.destroyForcibly(); + } + } catch (Exception ignored) { + // Ignore execution and setup exceptions + } + } +} diff --git a/sanitizers/src/test/java/com/example/ReflectiveCall.java b/sanitizers/src/test/java/com/example/ReflectiveCall.java index 7f85e486..e6b62b45 100644 --- a/sanitizers/src/test/java/com/example/ReflectiveCall.java +++ b/sanitizers/src/test/java/com/example/ReflectiveCall.java @@ -15,7 +15,6 @@ package com.example; import com.code_intelligence.jazzer.api.FuzzedDataProvider; -import java.lang.reflect.InvocationTargetException; public class ReflectiveCall { public static void fuzzerTestOneInput(FuzzedDataProvider data) { @@ -23,9 +22,8 @@ public class ReflectiveCall { if (input.startsWith("@")) { String className = input.substring(1); try { - Class.forName(className).getConstructor().newInstance(); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException - | NoSuchMethodException | ClassNotFoundException ignored) { + Class.forName(className); + } catch (ClassNotFoundException ignored) { } } } diff --git a/sanitizers/src/test/java/com/example/RegexCanonEqInjection.java b/sanitizers/src/test/java/com/example/RegexCanonEqInjection.java new file mode 100644 index 00000000..e2d0b722 --- /dev/null +++ b/sanitizers/src/test/java/com/example/RegexCanonEqInjection.java @@ -0,0 +1,41 @@ +// 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; +import java.util.regex.PatternSyntaxException; + +public class RegexCanonEqInjection { + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + String input = data.consumeRemainingAsString(); + try { + Pattern.compile(Pattern.quote(input), Pattern.CANON_EQ); + } catch (PatternSyntaxException ignored) { + } catch (IllegalArgumentException ignored) { + // "[媼" generates an IllegalArgumentException but only on Windows using + // Java 8. We ignore this for now. + // + // java.lang.IllegalArgumentException + // at java.lang.AbstractStringBuilder.appendCodePoint(AbstractStringBuilder.java:800) + // at java.lang.StringBuilder.appendCodePoint(StringBuilder.java:240) + // at java.util.regex.Pattern.normalizeCharClass(Pattern.java:1430) + // at java.util.regex.Pattern.normalize(Pattern.java:1396) + // at java.util.regex.Pattern.compile(Pattern.java:1665) + // at java.util.regex.Pattern.<init>(Pattern.java:1352) + // at java.util.regex.Pattern.compile(Pattern.java:1054) + } + } +} diff --git a/sanitizers/src/test/java/com/example/RegexInsecureQuoteInjection.java b/sanitizers/src/test/java/com/example/RegexInsecureQuoteInjection.java new file mode 100644 index 00000000..a548cfb2 --- /dev/null +++ b/sanitizers/src/test/java/com/example/RegexInsecureQuoteInjection.java @@ -0,0 +1,29 @@ +// 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; +import java.util.regex.PatternSyntaxException; + +public class RegexInsecureQuoteInjection { + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + String input = data.consumeRemainingAsString(); + try { + Pattern.matches("\\Q" + input + "\\E", "foobar"); + } catch (PatternSyntaxException ignored) { + } + } +} diff --git a/sanitizers/src/test/java/com/example/RegexRoadblocks.java b/sanitizers/src/test/java/com/example/RegexRoadblocks.java new file mode 100644 index 00000000..21986e3d --- /dev/null +++ b/sanitizers/src/test/java/com/example/RegexRoadblocks.java @@ -0,0 +1,89 @@ +// 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 com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow; +import java.util.regex.Pattern; + +public class RegexRoadblocks { + // We accept arbitrary suffixes but not prefixes for the following reasons: + // 1. The fuzzer will take much longer to match the exact length of the input than to satisfy the + // compare checks, which is what we really want to test. + // 2. Accepting arbitrary prefixes could lead to tests passing purely due to ToC entries being + // emitted in arbitrary positions, but we want to ensure that compares are correctly reported + // including position hints. + private static final Pattern LITERAL = Pattern.compile("foobarbaz.*"); + private static final Pattern QUOTED_LITERAL = Pattern.compile(Pattern.quote("jazzer_is_cool.*")); + private static final Pattern CASE_INSENSITIVE_LITERAL = + Pattern.compile("JaZzER!.*", Pattern.CASE_INSENSITIVE); + private static final Pattern GROUP = Pattern.compile("(always).*"); + private static final Pattern ALTERNATIVE = Pattern.compile("(to_be|not_to_be).*"); + private static final Pattern SINGLE_LATIN1_CHAR_PROPERTY = Pattern.compile("[€].*"); + private static final Pattern MULTIPLE_LATIN1_CHAR_PROPERTY = Pattern.compile("[ẞÄ].*"); + private static final Pattern RANGE_LATIN1_CHAR_PROPERTY = Pattern.compile("[¢-¥].*"); + + private static int run = 0; + + private static boolean matchedLiteral = false; + private static boolean matchedQuotedLiteral = false; + private static boolean matchedCaseInsensitiveLiteral = false; + private static boolean matchedGroup = false; + private static boolean matchedAlternative = false; + private static boolean matchedSingleLatin1CharProperty = false; + private static boolean matchedMultipleLatin1CharProperty = false; + private static boolean matchedRangeLatin1CharProperty = false; + + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + run++; + String input = data.consumeRemainingAsString(); + + if (!matchedLiteral && LITERAL.matcher(input).matches()) { + System.out.println("Cleared LITERAL"); + matchedLiteral = true; + } else if (!matchedQuotedLiteral && QUOTED_LITERAL.matcher(input).matches()) { + System.out.println("Cleared QUOTED_LITERAL"); + matchedQuotedLiteral = true; + } else if (!matchedCaseInsensitiveLiteral + && CASE_INSENSITIVE_LITERAL.matcher(input).matches()) { + System.out.println("Cleared CASE_INSENSITIVE_LITERAL"); + matchedCaseInsensitiveLiteral = true; + } else if (!matchedGroup && GROUP.matcher(input).matches()) { + System.out.println("Cleared GROUP"); + matchedGroup = true; + } else if (!matchedAlternative && ALTERNATIVE.matcher(input).matches()) { + System.out.println("Cleared ALTERNATIVE"); + matchedAlternative = true; + } else if (!matchedSingleLatin1CharProperty + && SINGLE_LATIN1_CHAR_PROPERTY.matcher(input).matches()) { + System.out.println("Cleared SINGLE_LATIN1_CHAR_PROPERTY"); + matchedSingleLatin1CharProperty = true; + } else if (!matchedMultipleLatin1CharProperty + && MULTIPLE_LATIN1_CHAR_PROPERTY.matcher(input).matches()) { + System.out.println("Cleared MULTIPLE_LATIN1_CHAR_PROPERTY"); + matchedMultipleLatin1CharProperty = true; + } else if (!matchedRangeLatin1CharProperty + && RANGE_LATIN1_CHAR_PROPERTY.matcher(input).matches()) { + System.out.println("Cleared RANGE_LATIN1_CHAR_PROPERTY"); + matchedRangeLatin1CharProperty = true; + } + + if (matchedLiteral && matchedQuotedLiteral && matchedCaseInsensitiveLiteral && matchedGroup + && matchedAlternative && matchedSingleLatin1CharProperty + && matchedMultipleLatin1CharProperty && matchedRangeLatin1CharProperty) { + throw new FuzzerSecurityIssueLow("Fuzzer matched all regexes in " + run + " runs"); + } + } +} diff --git a/sanitizers/src/test/java/com/example/SqlInjection.java b/sanitizers/src/test/java/com/example/SqlInjection.java new file mode 100644 index 00000000..8a16b5c8 --- /dev/null +++ b/sanitizers/src/test/java/com/example/SqlInjection.java @@ -0,0 +1,41 @@ +// 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.sql.Connection; +import java.sql.SQLException; +import org.h2.jdbcx.JdbcDataSource; + +public class SqlInjection { + static Connection conn = null; + + public static void fuzzerInitialize() throws Exception { + JdbcDataSource ds = new JdbcDataSource(); + ds.setURL("jdbc:h2:./test.db"); + conn = ds.getConnection(); + conn.createStatement().execute( + "CREATE TABLE IF NOT EXISTS pet (id IDENTITY PRIMARY KEY, name VARCHAR(50))"); + } + + static void insecureInsertUser(String userName) throws SQLException { + // Never use String.format instead of java.sql.Connection.prepareStatement ... + conn.createStatement().execute(String.format("INSERT INTO pet (name) VALUES ('%s')", userName)); + } + + public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Exception { + insecureInsertUser(data.consumeRemainingAsString()); + } +} diff --git a/sanitizers/src/main/java/jaz/Ter.java b/sanitizers/src/test/java/com/example/ldap/MockInitialContextFactory.java index 7814396f..b674f5c5 100644 --- a/sanitizers/src/main/java/jaz/Ter.java +++ b/sanitizers/src/test/java/com/example/ldap/MockInitialContextFactory.java @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package jaz; +package com.example.ldap; -/** - * A safe to use companion of {@link jaz.Zer} that is used to produce serializable instances of it - * with only light patching. - */ -@SuppressWarnings("unused") -public class Ter implements java.io.Serializable { - static final long serialVersionUID = 42L; +import java.util.Hashtable; +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.spi.InitialContextFactory; + +public class MockInitialContextFactory implements InitialContextFactory { + public Context getInitialContext(Hashtable environment) { + return new MockLdapContext(); + } } diff --git a/sanitizers/src/test/java/com/example/ldap/MockLdapContext.java b/sanitizers/src/test/java/com/example/ldap/MockLdapContext.java new file mode 100644 index 00000000..a51fadcd --- /dev/null +++ b/sanitizers/src/test/java/com/example/ldap/MockLdapContext.java @@ -0,0 +1,316 @@ +// Copyright 2021 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.ldap; + +import com.unboundid.ldap.sdk.DN; +import com.unboundid.ldap.sdk.Filter; +import com.unboundid.ldap.sdk.LDAPException; +import java.util.Hashtable; +import javax.naming.*; +import javax.naming.directory.*; +import javax.naming.ldap.*; + +/** + * Mock LdapContex implementation to test LdapInjection hook configuration. + * + * Only {@code com.example.ldap.MockLdapContext#search(java.lang.String, java.lang.String, + * javax.naming.directory.SearchControls)} is implemented to validate DN and filer query. + */ +public class MockLdapContext implements LdapContext { + @Override + public ExtendedResponse extendedOperation(ExtendedRequest request) throws NamingException { + return null; + } + + @Override + public LdapContext newInstance(Control[] requestControls) throws NamingException { + return this; + } + + @Override + public void reconnect(Control[] connCtls) throws NamingException {} + + @Override + public Control[] getConnectControls() throws NamingException { + return new Control[0]; + } + + @Override + public void setRequestControls(Control[] requestControls) throws NamingException {} + + @Override + public Control[] getRequestControls() throws NamingException { + return new Control[0]; + } + + @Override + public Control[] getResponseControls() throws NamingException { + return new Control[0]; + } + + @Override + public Attributes getAttributes(Name name) throws NamingException { + return null; + } + + @Override + public Attributes getAttributes(String name) throws NamingException { + return null; + } + + @Override + public Attributes getAttributes(Name name, String[] attrIds) throws NamingException { + return null; + } + + @Override + public Attributes getAttributes(String name, String[] attrIds) throws NamingException { + return null; + } + + @Override + public void modifyAttributes(Name name, int mod_op, Attributes attrs) throws NamingException {} + + @Override + public void modifyAttributes(String name, int mod_op, Attributes attrs) throws NamingException {} + + @Override + public void modifyAttributes(Name name, ModificationItem[] mods) throws NamingException {} + + @Override + public void modifyAttributes(String name, ModificationItem[] mods) throws NamingException {} + + @Override + public void bind(Name name, Object obj, Attributes attrs) throws NamingException {} + + @Override + public void bind(String name, Object obj, Attributes attrs) throws NamingException {} + + @Override + public void rebind(Name name, Object obj, Attributes attrs) throws NamingException {} + + @Override + public void rebind(String name, Object obj, Attributes attrs) throws NamingException {} + + @Override + public DirContext createSubcontext(Name name, Attributes attrs) throws NamingException { + return this; + } + + @Override + public DirContext createSubcontext(String name, Attributes attrs) throws NamingException { + return this; + } + + @Override + public DirContext getSchema(Name name) throws NamingException { + return this; + } + + @Override + public DirContext getSchema(String name) throws NamingException { + return this; + } + + @Override + public DirContext getSchemaClassDefinition(Name name) throws NamingException { + return this; + } + + @Override + public DirContext getSchemaClassDefinition(String name) throws NamingException { + return this; + } + + @Override + public NamingEnumeration<SearchResult> search(Name name, Attributes matchingAttributes, + String[] attributesToReturn) throws NamingException { + return null; + } + + @Override + public NamingEnumeration<SearchResult> search(String name, Attributes matchingAttributes, + String[] attributesToReturn) throws NamingException { + return null; + } + + @Override + public NamingEnumeration<SearchResult> search(Name name, Attributes matchingAttributes) + throws NamingException { + return null; + } + + @Override + public NamingEnumeration<SearchResult> search(String name, Attributes matchingAttributes) + throws NamingException { + return null; + } + + @Override + public NamingEnumeration<SearchResult> search(Name name, String filter, SearchControls cons) + throws NamingException { + return null; + } + + @Override + public NamingEnumeration<SearchResult> search(String name, String filter, SearchControls cons) + throws NamingException { + // Use UnboundID LDAP to validate DN and filter + if (!DN.isValidDN(name)) { + throw new NamingException("Invalid DN " + name); + } + try { + Filter.create(filter); + } catch (LDAPException e) { + throw new InvalidSearchFilterException("Invalid search filter " + filter); + } + return null; + } + + @Override + public NamingEnumeration<SearchResult> search(Name name, String filterExpr, Object[] filterArgs, + SearchControls cons) throws NamingException { + return null; + } + + @Override + public NamingEnumeration<SearchResult> search(String name, String filterExpr, Object[] filterArgs, + SearchControls cons) throws NamingException { + return null; + } + + @Override + public Object lookup(Name name) throws NamingException { + return this; + } + + @Override + public Object lookup(String name) throws NamingException { + return this; + } + + @Override + public void bind(Name name, Object obj) throws NamingException {} + + @Override + public void bind(String name, Object obj) throws NamingException {} + + @Override + public void rebind(Name name, Object obj) throws NamingException {} + + @Override + public void rebind(String name, Object obj) throws NamingException {} + + @Override + public void unbind(Name name) throws NamingException {} + + @Override + public void unbind(String name) throws NamingException {} + + @Override + public void rename(Name oldName, Name newName) throws NamingException {} + + @Override + public void rename(String oldName, String newName) throws NamingException {} + + @Override + public NamingEnumeration<NameClassPair> list(Name name) throws NamingException { + return null; + } + + @Override + public NamingEnumeration<NameClassPair> list(String name) throws NamingException { + return null; + } + + @Override + public NamingEnumeration<Binding> listBindings(Name name) throws NamingException { + return null; + } + + @Override + public NamingEnumeration<Binding> listBindings(String name) throws NamingException { + return null; + } + + @Override + public void destroySubcontext(Name name) throws NamingException {} + + @Override + public void destroySubcontext(String name) throws NamingException {} + + @Override + public Context createSubcontext(Name name) throws NamingException { + return this; + } + + @Override + public Context createSubcontext(String name) throws NamingException { + return this; + } + + @Override + public Object lookupLink(Name name) throws NamingException { + return this; + } + + @Override + public Object lookupLink(String name) throws NamingException { + return this; + } + + @Override + public NameParser getNameParser(Name name) throws NamingException { + return null; + } + + @Override + public NameParser getNameParser(String name) throws NamingException { + return null; + } + + @Override + public Name composeName(Name name, Name prefix) throws NamingException { + return null; + } + + @Override + public String composeName(String name, String prefix) throws NamingException { + return null; + } + + @Override + public Object addToEnvironment(String propName, Object propVal) throws NamingException { + return null; + } + + @Override + public Object removeFromEnvironment(String propName) throws NamingException { + return null; + } + + @Override + public Hashtable<?, ?> getEnvironment() throws NamingException { + return null; + } + + @Override + public void close() throws NamingException {} + + @Override + public String getNameInNamespace() throws NamingException { + return null; + } +} |