aboutsummaryrefslogtreecommitdiff
path: root/sanitizers/src/main/java/com/code_intelligence/jazzer
diff options
context:
space:
mode:
Diffstat (limited to 'sanitizers/src/main/java/com/code_intelligence/jazzer')
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel19
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt2
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt18
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt123
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt38
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt61
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt46
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt160
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java322
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.kt113
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Utils.kt14
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/BUILD.bazel7
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/ReflectionUtils.java62
13 files changed, 937 insertions, 48 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 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);
+ }
+}