aboutsummaryrefslogtreecommitdiff
path: root/sanitizers/src
diff options
context:
space:
mode:
Diffstat (limited to 'sanitizers/src')
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel59
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt55
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt17
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt20
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt6
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt7
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt3
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt21
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java20
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ScriptEngineInjection.java108
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ServerSideRequestForgery.java127
-rw-r--r--sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.java119
-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/XPathInjection.kt78
-rw-r--r--sanitizers/src/test/java/com/example/BUILD.bazel159
-rw-r--r--sanitizers/src/test/java/com/example/ClassLoaderLoadClass.java8
-rw-r--r--sanitizers/src/test/java/com/example/DisabledHooksTest.java110
-rw-r--r--sanitizers/src/test/java/com/example/ExpressionLanguageInjection.java31
-rw-r--r--sanitizers/src/test/java/com/example/LdapDnInjection.java15
-rw-r--r--sanitizers/src/test/java/com/example/LdapSearchInjection.java15
-rw-r--r--sanitizers/src/test/java/com/example/ReflectiveCall.java4
-rw-r--r--sanitizers/src/test/java/com/example/ScriptEngineInjection.java171
-rw-r--r--sanitizers/src/test/java/com/example/SsrfHttpClient.java39
-rw-r--r--sanitizers/src/test/java/com/example/SsrfSocketConnect.java (renamed from sanitizers/src/test/java/com/example/ldap/MockInitialContextFactory.java)19
-rw-r--r--sanitizers/src/test/java/com/example/SsrfSocketConnectToHost.java46
-rw-r--r--sanitizers/src/test/java/com/example/SsrfUrlConnection.java33
-rw-r--r--sanitizers/src/test/java/com/example/StackOverflowRegexInjection.java51
-rw-r--r--sanitizers/src/test/java/com/example/XPathInjection.java53
-rw-r--r--sanitizers/src/test/java/com/example/el/BUILD.bazel15
-rw-r--r--sanitizers/src/test/java/com/example/el/InsecureEmailValidator.java (renamed from sanitizers/src/test/java/com/example/InsecureEmailValidator.java)2
-rw-r--r--sanitizers/src/test/java/com/example/el/UserData.java41
31 files changed, 1297 insertions, 268 deletions
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel
index 1b156f9e..c2521b80 100644
--- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel
+++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel
@@ -1,12 +1,38 @@
+load("@bazel_skylib//rules:write_file.bzl", "write_file")
load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library")
+load("//bazel:kotlin.bzl", "ktlint")
+load("//sanitizers:sanitizers.bzl", "SANITIZER_CLASSES")
java_library(
name = "regex_roadblocks",
srcs = ["RegexRoadblocks.java"],
deps = [
- "//agent:jazzer_api_compile_only",
- "//agent/src/main/java/com/code_intelligence/jazzer/runtime:unsafe_provider",
"//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils:reflection_utils",
+ "//src/main/java/com/code_intelligence/jazzer/api:hooks",
+ "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider",
+ ],
+)
+
+java_library(
+ name = "server_side_request_forgery",
+ srcs = ["ServerSideRequestForgery.java"],
+ deps = ["//src/main/java/com/code_intelligence/jazzer/api:hooks"],
+)
+
+java_library(
+ name = "sql_injection",
+ srcs = ["SqlInjection.java"],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/api:hooks",
+ "@com_github_jsqlparser_jsqlparser//jar",
+ ],
+)
+
+java_library(
+ name = "script_engine_injection",
+ srcs = ["ScriptEngineInjection.java"],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/api:hooks",
],
)
@@ -20,15 +46,38 @@ kt_jvm_library(
"OsCommandInjection.kt",
"ReflectiveCall.kt",
"RegexInjection.kt",
- "SqlInjection.kt",
"Utils.kt",
+ "XPathInjection.kt",
],
visibility = ["//sanitizers:__pkg__"],
runtime_deps = [
":regex_roadblocks",
+ ":script_engine_injection",
+ ":server_side_request_forgery",
+ ":sql_injection",
],
deps = [
- "//agent:jazzer_api_compile_only",
- "@maven//:com_github_jsqlparser_jsqlparser",
+ "//src/main/java/com/code_intelligence/jazzer/api:hooks",
+ ],
+)
+
+java_library(
+ name = "constants",
+ srcs = [":constants_java"],
+ visibility = ["//visibility:public"],
+)
+
+write_file(
+ name = "constants_java",
+ out = "Constants.java",
+ content = [
+ "package com.code_intelligence.jazzer.sanitizers;",
+ "import java.util.Arrays;",
+ "import java.util.List;",
+ "public final class Constants {",
+ " public static final List<String> SANITIZER_HOOK_NAMES = Arrays.asList(%s);" % ", ".join(["\"%s\"" % name for name in SANITIZER_CLASSES]),
+ "}",
],
)
+
+ktlint()
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt
index 55691c1a..0ecbbf9f 100644
--- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt
+++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt
@@ -35,6 +35,12 @@ object Deserialization {
private val OBJECT_INPUT_STREAM_HEADER =
ObjectStreamConstants.STREAM_MAGIC.toBytes() + ObjectStreamConstants.STREAM_VERSION.toBytes()
+ init {
+ require(OBJECT_INPUT_STREAM_HEADER.size <= 64) {
+ "Object input stream header must fit in a table of recent compares entry (64 bytes)"
+ }
+ }
+
/**
* Used to memoize the [InputStream] used to construct a given [ObjectInputStream].
* [ThreadLocal] is required because the map is not synchronized (and likely cheaper than
@@ -57,13 +63,19 @@ object Deserialization {
// We can't instantiate jaz.Zer directly, so we instantiate and serialize jaz.Ter and then
// patch the class name.
val baos = ByteArrayOutputStream()
- ObjectOutputStream(baos).writeObject(jaz.Ter())
+ ObjectOutputStream(baos).writeObject(jaz.Ter(jaz.Ter.EXPRESSION_LANGUAGE_SANITIZER_ID))
val serializedJazTerInstance = baos.toByteArray()
val posToPatch = serializedJazTerInstance.indexOf("jaz.Ter".toByteArray())
serializedJazTerInstance[posToPatch + "jaz.".length] = 'Z'.code.toByte()
serializedJazTerInstance
}
+ init {
+ require(SERIALIZED_JAZ_ZER_INSTANCE.size <= 64) {
+ "Serialized jaz.Zer instance must fit in a table of recent compares entry (64 bytes)"
+ }
+ }
+
/**
* Guides the fuzzer towards producing a valid header for an ObjectInputStream.
*/
@@ -71,15 +83,16 @@ object Deserialization {
type = HookType.BEFORE,
targetClassName = "java.io.ObjectInputStream",
targetMethod = "<init>",
- targetMethodDescriptor = "(Ljava/io/InputStream;)V"
+ targetMethodDescriptor = "(Ljava/io/InputStream;)V",
)
@JvmStatic
fun objectInputStreamInitBeforeHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) {
val originalInputStream = args[0] as? InputStream ?: return
- val fixedInputStream = if (originalInputStream.markSupported())
+ val fixedInputStream = if (originalInputStream.markSupported()) {
originalInputStream
- else
+ } else {
BufferedInputStream(originalInputStream)
+ }
args[0] = fixedInputStream
guideMarkableInputStreamTowardsEquality(fixedInputStream, OBJECT_INPUT_STREAM_HEADER, hookId)
}
@@ -91,7 +104,7 @@ object Deserialization {
type = HookType.AFTER,
targetClassName = "java.io.ObjectInputStream",
targetMethod = "<init>",
- targetMethodDescriptor = "(Ljava/io/InputStream;)V"
+ targetMethodDescriptor = "(Ljava/io/InputStream;)V",
)
@JvmStatic
fun objectInputStreamInitAfterHook(
@@ -115,17 +128,17 @@ object Deserialization {
MethodHook(
type = HookType.BEFORE,
targetClassName = "java.io.ObjectInputStream",
- targetMethod = "readObject"
+ targetMethod = "readObject",
),
MethodHook(
type = HookType.BEFORE,
targetClassName = "java.io.ObjectInputStream",
- targetMethod = "readObjectOverride"
+ targetMethod = "readObjectOverride",
),
MethodHook(
type = HookType.BEFORE,
targetClassName = "java.io.ObjectInputStream",
- targetMethod = "readUnshared"
+ targetMethod = "readUnshared",
),
)
@JvmStatic
@@ -139,30 +152,4 @@ object Deserialization {
if (inputStream?.markSupported() != true) return
guideMarkableInputStreamTowardsEquality(inputStream, SERIALIZED_JAZ_ZER_INSTANCE, hookId)
}
-
- /**
- * Calls [Object.finalize] early if the returned object is [jaz.Zer]. A call to finalize is
- * guaranteed to happen at some point, but calling it early means that we can accurately report
- * the input that lead to its execution.
- */
- @MethodHooks(
- MethodHook(type = HookType.AFTER, targetClassName = "java.io.ObjectInputStream", targetMethod = "readObject"),
- MethodHook(type = HookType.AFTER, targetClassName = "java.io.ObjectInputStream", targetMethod = "readObjectOverride"),
- MethodHook(type = HookType.AFTER, targetClassName = "java.io.ObjectInputStream", targetMethod = "readUnshared"),
- )
- @JvmStatic
- fun readObjectAfterHook(
- method: MethodHandle?,
- objectInputStream: ObjectInputStream?,
- args: Array<Any?>,
- hookId: Int,
- deserializedObject: Any?,
- ) {
- if (deserializedObject?.javaClass?.name == HONEYPOT_CLASS_NAME) {
- deserializedObject.javaClass.getDeclaredMethod("finalize").run {
- isAccessible = true
- invoke(deserializedObject)
- }
- }
- }
}
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt
index 1dc1d5f0..a60c088e 100644
--- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt
+++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt
@@ -31,7 +31,13 @@ object ExpressionLanguageInjection {
* Try to call the default constructor of the honeypot class.
*/
private const val EXPRESSION_LANGUAGE_ATTACK =
- "\${\"\".getClass().forName(\"$HONEYPOT_CLASS_NAME\").newInstance()}"
+ "\${Byte.class.forName(\"$HONEYPOT_CLASS_NAME\").getMethod(\"el\").invoke(null)}"
+
+ init {
+ require(EXPRESSION_LANGUAGE_ATTACK.length <= 64) {
+ "Expression language exploit must fit in a table of recent compares entry (64 bytes)"
+ }
+ }
@MethodHooks(
MethodHook(
@@ -60,8 +66,10 @@ object ExpressionLanguageInjection {
method: MethodHandle?,
thisObject: Any?,
arguments: Array<Any>,
- hookId: Int
+ hookId: Int,
) {
+ // The overloads taking a second string argument have either three or four arguments
+ if (arguments.size < 3) { return }
val expression = arguments[1] as? String ?: return
Jazzer.guideTowardsContainment(expression, EXPRESSION_LANGUAGE_ATTACK, hookId)
}
@@ -76,15 +84,16 @@ object ExpressionLanguageInjection {
@MethodHook(
type = HookType.BEFORE,
targetClassName = "javax.validation.ConstraintValidatorContext",
- targetMethod = "buildConstraintViolationWithTemplate"
+ targetMethod = "buildConstraintViolationWithTemplate",
)
@JvmStatic
fun hookBuildConstraintViolationWithTemplate(
method: MethodHandle?,
thisObject: Any?,
arguments: Array<Any>,
- hookId: Int
+ hookId: Int,
) {
+ if (arguments.size != 1) { return }
val message = arguments[0] as String
Jazzer.guideTowardsContainment(message, EXPRESSION_LANGUAGE_ATTACK, hookId)
}
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt
index 1afd614e..76553e1a 100644
--- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt
+++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt
@@ -56,14 +56,14 @@ object LdapInjection {
targetClassName = "javax.naming.directory.DirContext",
targetMethod = "search",
targetMethodDescriptor = "(Ljava/lang/String;Ljavax/naming.directory/Attributes;)Ljavax/naming/NamingEnumeration;",
- additionalClassesToHook = ["javax.naming.directory.InitialDirContext"]
+ additionalClassesToHook = ["javax.naming.directory.InitialDirContext"],
),
MethodHook(
type = HookType.REPLACE,
targetClassName = "javax.naming.directory.DirContext",
targetMethod = "search",
targetMethodDescriptor = "(Ljava/lang/String;Ljavax/naming.directory/Attributes;[Ljava/lang/Sting;)Ljavax/naming/NamingEnumeration;",
- additionalClassesToHook = ["javax.naming.directory.InitialDirContext"]
+ additionalClassesToHook = ["javax.naming.directory.InitialDirContext"],
),
// Object search, possible DN and search filter injection
@@ -72,22 +72,22 @@ object LdapInjection {
targetClassName = "javax.naming.directory.DirContext",
targetMethod = "search",
targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/String;Ljavax/naming/directory/SearchControls;)Ljavax/naming/NamingEnumeration;",
- additionalClassesToHook = ["javax.naming.directory.InitialDirContext"]
+ additionalClassesToHook = ["javax.naming.directory.InitialDirContext"],
),
MethodHook(
type = HookType.REPLACE,
targetClassName = "javax.naming.directory.DirContext",
targetMethod = "search",
targetMethodDescriptor = "(Ljavax/naming/Name;Ljava/lang/String;[Ljava.lang.Object;Ljavax/naming/directory/SearchControls;)Ljavax/naming/NamingEnumeration;",
- additionalClassesToHook = ["javax.naming.directory.InitialDirContext"]
+ additionalClassesToHook = ["javax.naming.directory.InitialDirContext"],
),
MethodHook(
type = HookType.REPLACE,
targetClassName = "javax.naming.directory.DirContext",
targetMethod = "search",
targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;Ljavax/naming/directory/SearchControls;)Ljavax/naming/NamingEnumeration;",
- additionalClassesToHook = ["javax.naming.directory.InitialDirContext"]
- )
+ additionalClassesToHook = ["javax.naming.directory.InitialDirContext"],
+ ),
)
@JvmStatic
fun searchLdapContext(method: MethodHandle, thisObject: Any?, args: Array<Any>, hookId: Int): Any? {
@@ -106,15 +106,15 @@ object LdapInjection {
Jazzer.reportFindingFromHook(
FuzzerSecurityIssueCritical(
"""LDAP Injection
-Search filters based on untrusted data must be escape as specified in RFC 4515."""
- )
+Search filters based on untrusted data must be escaped as specified in RFC 4515.""",
+ ),
)
is NamingException ->
Jazzer.reportFindingFromHook(
FuzzerSecurityIssueCritical(
"""LDAP Injection
-Distinguished Names based on untrusted data must be escaped as specified in RFC 2253."""
- )
+Distinguished Names based on untrusted data must be escaped as specified in RFC 2253.""",
+ ),
)
}
throw e
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt
index 56e12f03..51cf6453 100644
--- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt
+++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt
@@ -49,14 +49,14 @@ object NamingContextLookup {
)
@JvmStatic
fun lookupHook(method: MethodHandle?, thisObject: Any?, args: Array<Any?>, hookId: Int): Any {
- val name = args[0] as String
+ val name = args[0] as? String ?: throw CommunicationException()
if (name.startsWith(RMI_MARKER) || name.startsWith(LDAP_MARKER)) {
Jazzer.reportFindingFromHook(
FuzzerSecurityIssueCritical(
"""Remote JNDI Lookup
JNDI lookups with attacker-controlled remote URLs can, depending on the JDK
-version, lead to remote code execution or the exfiltration of information."""
- )
+version, lead to remote code execution or the exfiltration of information.""",
+ ),
)
}
Jazzer.guideTowardsEquality(name, RMI_MARKER, hookId)
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt
index d3adc207..87de35c7 100644
--- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt
+++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt
@@ -39,10 +39,11 @@ object OsCommandInjection {
type = HookType.BEFORE,
targetClassName = "java.lang.ProcessImpl",
targetMethod = "start",
- additionalClassesToHook = ["java.lang.ProcessBuilder"]
+ additionalClassesToHook = ["java.lang.ProcessBuilder"],
)
@JvmStatic
fun processImplStartHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) {
+ if (args.isEmpty()) { return }
// Calling ProcessBuilder already checks if command array is empty
@Suppress("UNCHECKED_CAST")
(args[0] as? Array<String>)?.first().let { cmd ->
@@ -50,8 +51,8 @@ object OsCommandInjection {
Jazzer.reportFindingFromHook(
FuzzerSecurityIssueCritical(
"""OS Command Injection
-Executing OS commands with attacker-controlled data can lead to remote code execution."""
- )
+Executing OS commands with attacker-controlled data can lead to remote code execution.""",
+ ),
)
} else {
Jazzer.guideTowardsEquality(cmd, COMMAND, hookId)
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt
index 0fcabe36..62d58152 100644
--- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt
+++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt
@@ -61,10 +61,11 @@ object ReflectiveCall {
)
@JvmStatic
fun loadLibraryHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) {
+ if (args.isEmpty()) { return }
val libraryName = args[0] as? String ?: return
if (libraryName == HONEYPOT_LIBRARY_NAME) {
Jazzer.reportFindingFromHook(
- FuzzerSecurityIssueHigh("load arbitrary library")
+ FuzzerSecurityIssueHigh("load arbitrary library"),
)
}
Jazzer.guideTowardsEquality(libraryName, HONEYPOT_LIBRARY_NAME, hookId)
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt
index def5f6e3..5770f0c2 100644
--- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt
+++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt
@@ -23,6 +23,9 @@ import java.lang.invoke.MethodHandle
import java.util.regex.Pattern
import java.util.regex.PatternSyntaxException
+// message introduced in JDK14 and ported back to previous versions
+private const val STACK_OVERFLOW_ERROR_MESSAGE = "Stack overflow during pattern compilation"
+
@Suppress("unused_parameter", "unused")
object RegexInjection {
/**
@@ -43,7 +46,7 @@ object RegexInjection {
type = HookType.REPLACE,
targetClassName = "java.util.regex.Pattern",
targetMethod = "compile",
- targetMethodDescriptor = "(Ljava/lang/String;I)Ljava/util/regex/Pattern;"
+ targetMethodDescriptor = "(Ljava/lang/String;I)Ljava/util/regex/Pattern;",
)
@JvmStatic
fun compileWithFlagsHook(method: MethodHandle, alwaysNull: Any?, args: Array<Any?>, hookId: Int): Any? {
@@ -57,13 +60,13 @@ object RegexInjection {
type = HookType.REPLACE,
targetClassName = "java.util.regex.Pattern",
targetMethod = "compile",
- targetMethodDescriptor = "(Ljava/lang/String;)Ljava/util/regex/Pattern;"
+ targetMethodDescriptor = "(Ljava/lang/String;)Ljava/util/regex/Pattern;",
),
MethodHook(
type = HookType.REPLACE,
targetClassName = "java.util.regex.Pattern",
targetMethod = "matches",
- targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/CharSequence;)Z"
+ targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/CharSequence;)Z",
),
)
@JvmStatic
@@ -113,7 +116,7 @@ object RegexInjection {
pattern: String?,
hasCanonEqFlag: Boolean,
hookId: Int,
- vararg args: Any?
+ vararg args: Any?,
): Any? {
if (hasCanonEqFlag && pattern != null) {
// With CANON_EQ enabled, Pattern.compile allocates an array with a size that is
@@ -128,8 +131,8 @@ object RegexInjection {
"""Regular Expression Injection with CANON_EQ
When java.util.regex.Pattern.compile is used with the Pattern.CANON_EQ flag,
every injection into the regular expression pattern can cause arbitrarily large
-memory allocations, even when wrapped with Pattern.quote(...)."""
- )
+memory allocations, even when wrapped with Pattern.quote(...).""",
+ ),
)
} else {
Jazzer.guideTowardsContainment(pattern, CANON_EQ_ALMOST_EXPLOIT, hookId)
@@ -143,15 +146,15 @@ memory allocations, even when wrapped with Pattern.quote(...)."""
}
}
} catch (e: Exception) {
- if (e is PatternSyntaxException) {
+ if (e is PatternSyntaxException && !(e.message ?: "").startsWith(STACK_OVERFLOW_ERROR_MESSAGE)) {
Jazzer.reportFindingFromHook(
FuzzerSecurityIssueLow(
"""Regular Expression Injection
Regular expression patterns that contain unescaped untrusted input can consume
arbitrary amounts of CPU time. To properly escape the input, wrap it with
Pattern.quote(...).""",
- e
- )
+ e,
+ ),
)
}
throw e
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java
index 1043ac02..76c499b0 100644
--- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java
+++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java
@@ -22,7 +22,7 @@ import static com.code_intelligence.jazzer.sanitizers.utils.ReflectionUtils.offs
import com.code_intelligence.jazzer.api.HookType;
import com.code_intelligence.jazzer.api.Jazzer;
import com.code_intelligence.jazzer.api.MethodHook;
-import com.code_intelligence.jazzer.runtime.UnsafeProvider;
+import com.code_intelligence.jazzer.utils.UnsafeProvider;
import java.lang.invoke.MethodHandle;
import java.util.WeakHashMap;
import java.util.regex.Matcher;
@@ -65,13 +65,6 @@ public final class RegexRoadblocks {
private static final ThreadLocal<WeakHashMap<Object, Character>> PREDICATE_SOLUTIONS =
ThreadLocal.withInitial(WeakHashMap::new);
- // Do not act on instrumented regexes used by Jazzer internally, e.g. by ClassGraph.
- private static boolean HOOK_DISABLED = true;
-
- static {
- Jazzer.onFuzzTargetReady(() -> HOOK_DISABLED = UNSAFE == null);
- }
-
@MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$Node",
targetMethod = "match",
targetMethodDescriptor = "(Ljava/util/regex/Matcher;ILjava/lang/CharSequence;)Z",
@@ -122,7 +115,7 @@ public final class RegexRoadblocks {
})
public static void
nodeMatchHook(MethodHandle method, Object node, Object[] args, int hookId, Boolean matched) {
- if (HOOK_DISABLED || matched || node == null)
+ if (matched || node == null)
return;
Matcher matcher = (Matcher) args[0];
if (matcher == null)
@@ -211,7 +204,7 @@ public final class RegexRoadblocks {
additionalClassesToHook = {"java.util.regex.Pattern"})
public static void
singleHook(MethodHandle method, Object node, Object[] args, int hookId, Object predicate) {
- if (HOOK_DISABLED || predicate == null)
+ if (predicate == null)
return;
PREDICATE_SOLUTIONS.get().put(predicate, (char) (int) args[0]);
}
@@ -229,7 +222,7 @@ public final class RegexRoadblocks {
public static void
java8SingleHook(
MethodHandle method, Object property, Object[] args, int hookId, Object alwaysNull) {
- if (HOOK_DISABLED || property == null)
+ if (property == null)
return;
PREDICATE_SOLUTIONS.get().put(property, (char) (int) args[0]);
}
@@ -258,7 +251,7 @@ public final class RegexRoadblocks {
additionalClassesToHook = {"java.util.regex.Pattern"})
public static void
rangeHook(MethodHandle method, Object node, Object[] args, int hookId, Object predicate) {
- if (HOOK_DISABLED || predicate == null)
+ if (predicate == null)
return;
PREDICATE_SOLUTIONS.get().put(predicate, (char) (int) args[0]);
}
@@ -280,7 +273,7 @@ public final class RegexRoadblocks {
public static void
unionHook(
MethodHandle method, Object thisObject, Object[] args, int hookId, Object unionPredicate) {
- if (HOOK_DISABLED || unionPredicate == null)
+ if (unionPredicate == null)
return;
Character solution = predicateSolution(thisObject);
if (solution == null)
@@ -298,7 +291,6 @@ public final class RegexRoadblocks {
boolean[] bits = (boolean[]) UNSAFE.getObject(charPredicate, BIT_CLASS_BITS_OFFSET);
for (int i = 0; i < bits.length; i++) {
if (bits[i]) {
- PREDICATE_SOLUTIONS.get().put(charPredicate, (char) i);
return (char) i;
}
}
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ScriptEngineInjection.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ScriptEngineInjection.java
new file mode 100644
index 00000000..6f084bf9
--- /dev/null
+++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ScriptEngineInjection.java
@@ -0,0 +1,108 @@
+// Copyright 2023 Code Intelligence GmbH
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.code_intelligence.jazzer.sanitizers;
+
+import com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical;
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.Jazzer;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.lang.invoke.MethodHandle;
+
+/**
+ * Detects Script Engine injections.
+ *
+ * <p>
+ * The hooks in this class attempt to detect user input flowing into
+ * {@link javax.script.ScriptEngine#eval(String)} and the like that might lead
+ * to remote code executions depending on the scripting engine's capabilities.
+ * Before JDK 15, the Nashorn Engine was registered by default with
+ * ScriptEngineManager under several aliases, including "js". Nashorn allows
+ * access to JVM classes, for example {@link java.lang.Runtime} allowing the
+ * execution of arbitrary OS commands. Several other scripting engines can be
+ * embedded to the JVM (they must follow the
+ * <a href="https://www.jcp.org/en/jsr/detail?id=223">JSR-223 </a>
+ * specification).
+ **/
+@SuppressWarnings("unused")
+public final class ScriptEngineInjection {
+ private static final String PAYLOAD = "\"jaz\"+\"zer\"";
+
+ /**
+ * String variants of eval can be intercepted by before hooks, as the script
+ * content can directly be checked for the presence of the payload.
+ */
+ @MethodHook(type = HookType.BEFORE, targetClassName = "javax.script.ScriptEngine",
+ targetMethod = "eval", targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "javax.script.ScriptEngine",
+ targetMethod = "eval",
+ targetMethodDescriptor = "(Ljava/lang/String;Ljavax/script/ScriptContext;)Ljava/lang/Object;")
+ @MethodHook(type = HookType.BEFORE, targetClassName = "javax.script.ScriptEngine",
+ targetMethod = "eval",
+ targetMethodDescriptor = "(Ljava/lang/String;Ljavax/script/Bindings;)Ljava/lang/Object;")
+ public static void
+ checkScriptEngineExecuteString(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ checkScriptContent((String) arguments[0], hookId);
+ }
+
+ /**
+ * Reader variants of eval must be intercepted by replace hooks, as their
+ * contents are converted to strings, for the payload check, and back to readers
+ * for the actual method invocation.
+ */
+ @MethodHook(type = HookType.REPLACE, targetClassName = "javax.script.ScriptEngine",
+ targetMethod = "eval", targetMethodDescriptor = "(Ljava/io/Reader;)Ljava/lang/Object;")
+ @MethodHook(type = HookType.REPLACE, targetClassName = "javax.script.ScriptEngine",
+ targetMethod = "eval",
+ targetMethodDescriptor = "(Ljava/io/Reader;Ljavax/script/ScriptContext;)Ljava/lang/Object;")
+ @MethodHook(type = HookType.REPLACE, targetClassName = "javax.script.ScriptEngine",
+ targetMethod = "eval",
+ targetMethodDescriptor = "(Ljava/io/Reader;Ljavax/script/Bindings;)Ljava/lang/Object;")
+ public static Object
+ checkScriptEngineExecute(MethodHandle method, Object thisObject, Object[] arguments, int hookId)
+ throws Throwable {
+ if (arguments[0] != null) {
+ String content = readAll((Reader) arguments[0]);
+ checkScriptContent(content, hookId);
+ arguments[0] = new StringReader(content);
+ }
+ return method.invokeWithArguments(thisObject, arguments);
+ }
+
+ private static void checkScriptContent(String content, int hookId) {
+ if (content != null) {
+ if (content.contains(PAYLOAD)) {
+ Jazzer.reportFindingFromHook(new FuzzerSecurityIssueCritical(
+ "Script Engine Injection: Insecure user input was used in script engine invocation.\n"
+ + "Depending on the script engine's capabilities this could lead to sandbox escape and remote code execution."));
+ } else {
+ Jazzer.guideTowardsContainment(content, PAYLOAD, hookId);
+ }
+ }
+ }
+
+ private static String readAll(Reader reader) throws IOException {
+ StringBuilder content = new StringBuilder();
+ char[] buffer = new char[4096];
+ int numChars;
+ while ((numChars = reader.read(buffer)) >= 0) {
+ content.append(buffer, 0, numChars);
+ }
+ return content.toString();
+ }
+}
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ServerSideRequestForgery.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ServerSideRequestForgery.java
new file mode 100644
index 00000000..3ff48e3c
--- /dev/null
+++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ServerSideRequestForgery.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.code_intelligence.jazzer.sanitizers;
+
+import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh;
+import com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium;
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.Jazzer;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.lang.invoke.MethodHandle;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiPredicate;
+
+public class ServerSideRequestForgery {
+ // Set via reflection by Jazzer's BugDetectors API.
+ public static final AtomicReference<BiPredicate<String, Integer>> connectionPermitted =
+ new AtomicReference<>((host, port) -> false);
+
+ /**
+ * {@link java.net.Socket} is used in many JDK classes to open network connections. Internally it
+ * delegates to {@link java.net.SocketImpl}, hence, for most situations it's sufficient to hook
+ * the call site {@link java.net.Socket} itself. As {@link java.net.SocketImpl} is an abstract
+ * class all call sites invoking "connect" on concrete implementations get hooked. As JKD internal
+ * classes are normally ignored, they have to be marked for hooking explicitly. In this case, all
+ * internal classes calling "connect" on {@link java.net.SocketImpl} should be listed below.
+ * Internal classes using {@link java.net.SocketImpl#connect(String, int)}:
+ * <ul>
+ * <li>java.net.Socket (hook required)
+ * <li>java.net.AbstractPlainSocketImpl (no direct usage, no hook required)
+ * <li>java.net.PlainSocketImpl (no direct usage, no hook required)
+ * <li>java.net.HttpConnectSocketImpl (only used in Socket, which is already listed)
+ * <li>java.net.SocksSocketImpl (used in Socket, but also invoking super.connect directly,
+ * hook required)
+ * <li>java.net.ServerSocket (security check, no hook required)
+ * </ul>
+ */
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.net.SocketImpl",
+ targetMethod = "connect",
+ additionalClassesToHook =
+ {
+ "java.net.Socket",
+ "java.net.SocksSocketImpl",
+ })
+ public static void
+ checkSsrfSocket(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ checkSsrf(arguments);
+ }
+
+ /**
+ * {@link java.nio.channels.SocketChannel} is used in many JDK classes to open (non-blocking)
+ * network connections, e.g. {@link java.net.http.HttpClient} uses it internally. The actual
+ * connection is established in the abstract "connect" method. Hooking that also hooks invocations
+ * of all concrete implementations, from which only one exists in {@link
+ * sun.nio.ch.SocketChannelImpl}. "connect" is only called in {@link
+ * java.nio.channels.SocketChannel} itself and the two mentioned classes below.
+ */
+ @MethodHook(type = HookType.BEFORE, targetClassName = "java.nio.channels.SocketChannel",
+ targetMethod = "connect",
+ additionalClassesToHook =
+ {
+ "sun.nio.ch.SocketAdaptor",
+ "jdk.internal.net.http.PlainHttpConnection",
+ })
+ public static void
+ checkSsrfHttpConnection(MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ checkSsrf(arguments);
+ }
+
+ private static void checkSsrf(Object[] arguments) {
+ if (arguments.length == 0) {
+ return;
+ }
+
+ String host;
+ int port;
+ if (arguments[0] instanceof InetSocketAddress) {
+ // Only implementation of java.net.SocketAddress.
+ InetSocketAddress address = (InetSocketAddress) arguments[0];
+ host = address.getHostName();
+ port = address.getPort();
+ } else if (arguments.length >= 2 && arguments[1] instanceof Integer) {
+ if (arguments[0] instanceof InetAddress) {
+ host = ((InetAddress) arguments[0]).getHostName();
+ } else if (arguments[0] instanceof String) {
+ host = (String) arguments[0];
+ } else {
+ return;
+ }
+ port = (int) arguments[1];
+ } else {
+ return;
+ }
+
+ if (port < 0 || port > 65535) {
+ return;
+ }
+
+ if (!connectionPermitted.get().test(host, port)) {
+ Jazzer.reportFindingFromHook(new FuzzerSecurityIssueMedium(String.format(
+ "Server Side Request Forgery (SSRF)\n"
+ + "Attempted connection to: %s:%d\n"
+ + "Requests to destinations based on untrusted data could lead to exfiltration of "
+ + "sensitive data or exposure of internal services.\n\n"
+ + "If the fuzz test is expected to perform network connections, call "
+ + "com.code_intelligence.jazzer.api.BugDetectors#allowNetworkConnections at the "
+ + "beginning of your fuzz test and optionally provide a predicate matching the "
+ + "expected hosts.",
+ host, port)));
+ }
+ }
+}
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.java
new file mode 100644
index 00000000..da5beaa8
--- /dev/null
+++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.java
@@ -0,0 +1,119 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.code_intelligence.jazzer.sanitizers;
+
+import static java.util.Collections.unmodifiableSet;
+import static java.util.stream.Collectors.toSet;
+
+import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh;
+import com.code_intelligence.jazzer.api.HookType;
+import com.code_intelligence.jazzer.api.Jazzer;
+import com.code_intelligence.jazzer.api.MethodHook;
+import java.lang.invoke.MethodHandle;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Stream;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+
+/**
+ * Detects SQL injections.
+ *
+ * <p>Untrusted input has to be escaped in such a way that queries remain valid otherwise an
+ * injection could be possible. This sanitizer guides the fuzzer to inject insecure characters. If
+ * an exception is raised during execution the fuzzer was able to inject an invalid pattern,
+ * otherwise all input was escaped correctly.
+ *
+ * <p>Two types of methods are hooked:
+ * <ol>
+ * <li>Methods that take an SQL query as the first argument (e.g. {@link
+ * java.sql.Statement#execute}]).</li>
+ * <li>Methods that don't take any arguments and execute an already prepared statement (e.g. {@link
+ * java.sql.PreparedStatement#execute}).</li>
+ * </ol>
+ *
+ * For 1. we validate the syntax of the query using <a
+ * href="https://github.com/JSQLParser/JSqlParser">jsqlparser</a> and if both the syntax is invalid
+ * and the query execution throws an exception we report an SQL injection. Since we can't reliably
+ * validate SQL queries in arbitrary dialects this hook is expected to produce some amount of false
+ * positives. For 2. we can't validate the query syntax and therefore only rethrow any exceptions.
+ */
+@SuppressWarnings("unused")
+public class SqlInjection {
+ // Characters that should be escaped in user input.
+ // See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html
+ private static final String CHARACTERS_TO_ESCAPE = "'\"\b\n\r\t\\%_";
+
+ private static final Set<String> SQL_SYNTAX_ERROR_EXCEPTIONS = unmodifiableSet(
+ Stream
+ .of("java.sql.SQLException", "java.sql.SQLNonTransientException",
+ "java.sql.SQLSyntaxErrorException", "org.h2.jdbc.JdbcSQLSyntaxErrorException",
+ "org.h2.jdbc.JdbcSQLFeatureNotSupportedException")
+ .collect(toSet()));
+
+ @MethodHook(
+ type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "execute")
+ @MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement",
+ targetMethod = "executeBatch")
+ @MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement",
+ targetMethod = "executeLargeBatch")
+ @MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement",
+ targetMethod = "executeLargeUpdate")
+ @MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement",
+ targetMethod = "executeQuery")
+ @MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement",
+ targetMethod = "executeUpdate")
+ @MethodHook(type = HookType.REPLACE, targetClassName = "javax.persistence.EntityManager",
+ targetMethod = "createNativeQuery")
+ public static Object
+ checkSqlExecute(MethodHandle method, Object thisObject, Object[] arguments, int hookId)
+ throws Throwable {
+ boolean hasValidSqlQuery = false;
+
+ if (arguments.length > 0 && arguments[0] instanceof String) {
+ String query = (String) arguments[0];
+ hasValidSqlQuery = isValidSql(query);
+ Jazzer.guideTowardsContainment(query, CHARACTERS_TO_ESCAPE, hookId);
+ }
+ try {
+ return method.invokeWithArguments(
+ Stream.concat(Stream.of(thisObject), Arrays.stream(arguments)).toArray());
+ } catch (Throwable throwable) {
+ // If we already validated the query string and know it's correct,
+ // The exception is likely thrown by a non-existent table or something
+ // that we don't want to report.
+ if (!hasValidSqlQuery
+ && SQL_SYNTAX_ERROR_EXCEPTIONS.contains(throwable.getClass().getName())) {
+ Jazzer.reportFindingFromHook(new FuzzerSecurityIssueHigh(
+ String.format("SQL Injection%nInjected query: %s%n", arguments[0])));
+ }
+ throw throwable;
+ }
+ }
+
+ private static boolean isValidSql(String sql) {
+ try {
+ CCJSqlParserUtil.parseStatements(sql);
+ return true;
+ } catch (JSQLParserException e) {
+ return false;
+ } catch (Throwable t) {
+ // Catch any unexpected exceptions so that we don't disturb the
+ // instrumented application.
+ t.printStackTrace();
+ return true;
+ }
+ }
+}
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.kt
deleted file mode 100644
index f317bcc8..00000000
--- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.kt
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright 2022 Code Intelligence GmbH
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.code_intelligence.jazzer.sanitizers
-
-import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh
-import com.code_intelligence.jazzer.api.HookType
-import com.code_intelligence.jazzer.api.Jazzer
-import com.code_intelligence.jazzer.api.MethodHook
-import com.code_intelligence.jazzer.api.MethodHooks
-import net.sf.jsqlparser.JSQLParserException
-import net.sf.jsqlparser.parser.CCJSqlParserUtil
-import java.lang.invoke.MethodHandle
-
-/**
- * Detects SQL injections.
- *
- * Untrusted input has to be escaped in such a way that queries remain valid otherwise an injection
- * could be possible. This sanitizer guides the fuzzer to inject insecure characters. If an exception
- * is raised during execution the fuzzer was able to inject an invalid pattern, otherwise all input
- * was escaped correctly.
- *
- * Two types of methods are hooked:
- * 1. Methods that take an SQL query as the first argument (e.g. [java.sql.Statement.execute]).
- * 2. Methods that don't take any arguments and execute an already prepared statement
- * (e.g. [java.sql.PreparedStatement.execute]).
- * For 1. we validate the syntax of the query using <a href="https://github.com/JSQLParser/JSqlParser">jsqlparser</a>
- * and if both the syntax is invalid and the query execution throws an exception we report an SQL injection.
- * Since we can't reliably validate SQL queries in arbitrary dialects this hook is expected to produce some
- * amount of false positives.
- * For 2. we can't validate the query syntax and therefore only rethrow any exceptions.
- */
-@Suppress("unused_parameter", "unused")
-object SqlInjection {
-
- // Characters that should be escaped in user input.
- // See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html
- private const val CHARACTERS_TO_ESCAPE = "'\"\b\n\r\t\\%_"
-
- private val SQL_SYNTAX_ERROR_EXCEPTIONS = listOf(
- "java.sql.SQLException",
- "java.sql.SQLNonTransientException",
- "java.sql.SQLSyntaxErrorException",
- "org.h2.jdbc.JdbcSQLSyntaxErrorException",
- )
-
- @MethodHooks(
- MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "execute"),
- MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeBatch"),
- MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeLargeBatch"),
- MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeLargeUpdate"),
- MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeQuery"),
- MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeUpdate"),
- MethodHook(
- type = HookType.REPLACE,
- targetClassName = "javax.persistence.EntityManager",
- targetMethod = "createNativeQuery"
- )
- )
- @JvmStatic
- fun checkSqlExecute(method: MethodHandle, thisObject: Any?, arguments: Array<Any>, hookId: Int): Any {
- var hasValidSqlQuery = false
-
- if (arguments.isNotEmpty() && arguments[0] is String) {
- val query = arguments[0] as String
- hasValidSqlQuery = isValidSql(query)
- Jazzer.guideTowardsContainment(query, CHARACTERS_TO_ESCAPE, hookId)
- }
- return try {
- method.invokeWithArguments(thisObject, *arguments)
- } catch (throwable: Throwable) {
- // If we already validated the query string and know it's correct,
- // The exception is likely thrown by a non-existent table or something
- // that we don't want to report.
- if (!hasValidSqlQuery && SQL_SYNTAX_ERROR_EXCEPTIONS.contains(throwable.javaClass.name)) {
- Jazzer.reportFindingFromHook(
- FuzzerSecurityIssueHigh(
- """
- SQL Injection
- Injected query: ${arguments[0]}
- """.trimIndent(),
- throwable
- )
- )
- }
- throw throwable
- }
- }
-
- private fun isValidSql(sql: String): Boolean =
- try {
- CCJSqlParserUtil.parseStatements(sql)
- true
- } catch (e: JSQLParserException) {
- false
- } catch (t: Throwable) {
- // Catch any unexpected exceptions so that we don't disturb the
- // instrumented application.
- t.printStackTrace()
- true
- }
-}
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/XPathInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/XPathInjection.kt
new file mode 100644
index 00000000..b54d0839
--- /dev/null
+++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/XPathInjection.kt
@@ -0,0 +1,78 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.code_intelligence.jazzer.sanitizers
+
+import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh
+import com.code_intelligence.jazzer.api.HookType
+import com.code_intelligence.jazzer.api.Jazzer
+import com.code_intelligence.jazzer.api.MethodHook
+import com.code_intelligence.jazzer.api.MethodHooks
+import java.lang.invoke.MethodHandle
+import javax.xml.xpath.XPathExpressionException
+
+/**
+ * Detects XPath injections.
+ *
+ * Untrusted input has to be escaped in such a way that queries remain valid, otherwise an injection
+ * could be possible. This sanitizer guides the fuzzer to inject insecure characters. If an exception
+ * is raised during execution the fuzzer was able to inject an invalid pattern, otherwise all input
+ * was escaped correctly.
+ * Checking if the innermost cause of XPathExpressionException is a TransformerException should
+ * indicate injection instead of a false positive.
+ */
+@Suppress("unused_parameter", "unused")
+object XPathInjection {
+
+ // Characters that should be escaped in user input.
+ // https://owasp.org/www-community/attacks/XPATH_Injection
+ private const val CHARACTERS_TO_ESCAPE = "'\""
+
+ private val XPATH_SYNTAX_ERROR_EXCEPTIONS = "javax.xml.transform.TransformerException"
+
+ @MethodHooks(
+ MethodHook(type = HookType.REPLACE, targetClassName = "javax.xml.xpath.XPath", targetMethod = "compile"),
+ MethodHook(type = HookType.REPLACE, targetClassName = "javax.xml.xpath.XPath", targetMethod = "evaluate"),
+ MethodHook(type = HookType.REPLACE, targetClassName = "javax.xml.xpath.XPath", targetMethod = "evaluateExpression"),
+ )
+ @JvmStatic
+ fun checkXpathExecute(method: MethodHandle, thisObject: Any?, arguments: Array<Any>, hookId: Int): Any {
+ if (arguments.isNotEmpty() && arguments[0] is String) {
+ val query = arguments[0] as String
+ Jazzer.guideTowardsContainment(query, CHARACTERS_TO_ESCAPE, hookId)
+ }
+ return try {
+ method.invokeWithArguments(thisObject, *arguments)
+ } catch (exception: XPathExpressionException) {
+ // find innermost cause
+ var innerCause = exception.cause
+ while (innerCause?.cause != null && innerCause.cause != innerCause) {
+ innerCause = innerCause.cause
+ }
+
+ if (innerCause != null && XPATH_SYNTAX_ERROR_EXCEPTIONS.equals(innerCause.javaClass.name)) {
+ Jazzer.reportFindingFromHook(
+ FuzzerSecurityIssueHigh(
+ """
+ XPath Injection
+ Injected query: ${arguments[0]}
+ """.trimIndent(),
+ exception,
+ ),
+ )
+ }
+ throw exception
+ }
+ }
+}
diff --git a/sanitizers/src/test/java/com/example/BUILD.bazel b/sanitizers/src/test/java/com/example/BUILD.bazel
index 5d2e1ca5..ea0a7f82 100644
--- a/sanitizers/src/test/java/com/example/BUILD.bazel
+++ b/sanitizers/src/test/java/com/example/BUILD.bazel
@@ -6,7 +6,10 @@ java_fuzz_target_test(
srcs = [
"ObjectInputStreamDeserialization.java",
],
- expected_findings = ["java.lang.ExceptionInInitializerError"],
+ allowed_findings = [
+ "com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh",
+ "java.lang.ExceptionInInitializerError",
+ ],
target_class = "com.example.ObjectInputStreamDeserialization",
)
@@ -15,7 +18,10 @@ java_fuzz_target_test(
srcs = [
"ReflectiveCall.java",
],
- expected_findings = ["java.lang.ExceptionInInitializerError"],
+ allowed_findings = [
+ "com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh",
+ "java.lang.ExceptionInInitializerError",
+ ],
target_class = "com.example.ReflectiveCall",
)
@@ -24,25 +30,30 @@ java_fuzz_target_test(
srcs = [
"LibraryLoad.java",
],
+ allowed_findings = [
+ "com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh",
+ ],
target_class = "com.example.LibraryLoad",
# loading of native libraries is very slow on macos,
# especially using Java 17
target_compatible_with = SKIP_ON_MACOS,
+ # The reproducer doesn't contain the sanitizer and thus runs into an ordinary ignored
+ # UnsatisfiedLinkError.
+ verify_crash_reproducer = False,
)
java_fuzz_target_test(
name = "ExpressionLanguageInjection",
srcs = [
"ExpressionLanguageInjection.java",
- "InsecureEmailValidator.java",
],
+ allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh"],
target_class = "com.example.ExpressionLanguageInjection",
+ # The reproducer can't find jaz.Zer and thus doesn't crash.
+ verify_crash_reproducer = False,
deps = [
- "@maven//:javax_el_javax_el_api",
+ "//sanitizers/src/test/java/com/example/el:ExpressionLanguageExample",
"@maven//:javax_validation_validation_api",
- "@maven//:javax_xml_bind_jaxb_api",
- "@maven//:org_glassfish_javax_el",
- "@maven//:org_hibernate_hibernate_validator",
],
)
@@ -51,7 +62,9 @@ java_fuzz_target_test(
srcs = [
"OsCommandInjectionProcessBuilder.java",
],
+ allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical"],
target_class = "com.example.OsCommandInjectionProcessBuilder",
+ verify_crash_reproducer = False,
)
java_fuzz_target_test(
@@ -59,17 +72,22 @@ java_fuzz_target_test(
srcs = [
"OsCommandInjectionRuntimeExec.java",
],
+ allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical"],
target_class = "com.example.OsCommandInjectionRuntimeExec",
+ verify_crash_reproducer = False,
)
java_fuzz_target_test(
name = "LdapSearchInjection",
srcs = [
"LdapSearchInjection.java",
- "ldap/MockInitialContextFactory.java",
"ldap/MockLdapContext.java",
],
- expected_findings = ["javax.naming.directory.InvalidSearchFilterException"],
+ allowed_findings = [
+ "com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical",
+ # The crashing input encoded by the replayer does not have valid syntax, but no hook.
+ "javax.naming.directory.InvalidSearchFilterException",
+ ],
target_class = "com.example.LdapSearchInjection",
deps = [
"@maven//:com_unboundid_unboundid_ldapsdk",
@@ -80,10 +98,13 @@ java_fuzz_target_test(
name = "LdapDnInjection",
srcs = [
"LdapDnInjection.java",
- "ldap/MockInitialContextFactory.java",
"ldap/MockLdapContext.java",
],
- expected_findings = ["javax.naming.NamingException"],
+ allowed_findings = [
+ "com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical",
+ # The crashing input encoded by the reproducer does not have valid syntax, but no hook.
+ "javax.naming.NamingException",
+ ],
target_class = "com.example.LdapDnInjection",
deps = [
"@maven//:com_unboundid_unboundid_ldapsdk",
@@ -93,7 +114,9 @@ java_fuzz_target_test(
java_fuzz_target_test(
name = "RegexInsecureQuoteInjection",
srcs = ["RegexInsecureQuoteInjection.java"],
+ allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"],
target_class = "com.example.RegexInsecureQuoteInjection",
+ verify_crash_reproducer = False,
)
java_fuzz_target_test(
@@ -101,7 +124,9 @@ java_fuzz_target_test(
srcs = [
"RegexCanonEqInjection.java",
],
+ allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"],
target_class = "com.example.RegexCanonEqInjection",
+ verify_crash_reproducer = False,
)
java_fuzz_target_test(
@@ -109,19 +134,40 @@ java_fuzz_target_test(
srcs = [
"ClassLoaderLoadClass.java",
],
- expected_findings = ["java.lang.ExceptionInInitializerError"],
+ allowed_findings = [
+ "com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh",
+ # Reproducer does not find the honeypot library and doesn't have the hook.
+ "java.lang.ExceptionInInitializerError",
+ ],
target_class = "com.example.ClassLoaderLoadClass",
)
java_fuzz_target_test(
name = "RegexRoadblocks",
srcs = ["RegexRoadblocks.java"],
+ allowed_findings = ["com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow"],
fuzzer_args = [
# Limit the number of runs to verify that the regex roadblocks are
# cleared quickly.
"-runs=22000",
],
target_class = "com.example.RegexRoadblocks",
+ verify_crash_reproducer = False,
+)
+
+# Catching StackOverflowErrors doesn't work reliably across all systems and JDK versions.
+# It may lead to a native crash before we can handle the exception in Java, therefore the
+# test is set to manual execution.
+java_fuzz_target_test(
+ name = "StackOverflowRegexInjection",
+ srcs = ["StackOverflowRegexInjection.java"],
+ allowed_findings = ["java.util.regex.PatternSyntaxException"],
+ fuzzer_args = [
+ "-runs=1",
+ ],
+ tags = ["manual"],
+ target_class = "com.example.StackOverflowRegexInjection",
+ verify_crash_reproducer = False,
)
java_fuzz_target_test(
@@ -129,7 +175,7 @@ java_fuzz_target_test(
srcs = [
"SqlInjection.java",
],
- expected_findings = [
+ allowed_findings = [
"com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh",
"org.h2.jdbc.JdbcSQLSyntaxErrorException",
],
@@ -138,3 +184,90 @@ java_fuzz_target_test(
"@maven//:com_h2database_h2",
],
)
+
+java_test(
+ name = "DisabledHooksTest",
+ size = "small",
+ srcs = [
+ "DisabledHooksTest.java",
+ ],
+ test_class = "com.example.DisabledHooksTest",
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/api",
+ "//src/main/java/com/code_intelligence/jazzer/api:hooks",
+ ],
+)
+
+java_fuzz_target_test(
+ name = "XPathInjection",
+ srcs = [
+ "XPathInjection.java",
+ ],
+ allowed_findings = [
+ "com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh",
+ ],
+ target_class = "com.example.XPathInjection",
+ # Fuzz target catches the syntax exception triggered by the reproducer without the sanitizer.
+ verify_crash_reproducer = False,
+)
+
+java_fuzz_target_test(
+ name = "SsrfSocketConnect",
+ srcs = [
+ "SsrfSocketConnect.java",
+ ],
+ allowed_findings = [
+ "com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium",
+ ],
+ target_class = "com.example.SsrfSocketConnect",
+ verify_crash_reproducer = False,
+)
+
+java_fuzz_target_test(
+ name = "SsrfSocketConnectToHost",
+ srcs = [
+ "SsrfSocketConnectToHost.java",
+ ],
+ allowed_findings = [
+ "com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium",
+ ],
+ target_class = "com.example.SsrfSocketConnectToHost",
+ verify_crash_reproducer = False,
+)
+
+java_fuzz_target_test(
+ name = "SsrfUrlConnection",
+ srcs = [
+ "SsrfUrlConnection.java",
+ ],
+ allowed_findings = [
+ "com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium",
+ ],
+ target_class = "com.example.SsrfUrlConnection",
+ verify_crash_reproducer = False,
+)
+
+java_fuzz_target_test(
+ name = "SsrfHttpClient",
+ srcs = [
+ "SsrfHttpClient.java",
+ ],
+ allowed_findings = [
+ "com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium",
+ ],
+ tags = ["no-jdk8"],
+ target_class = "com.example.SsrfHttpClient",
+ verify_crash_reproducer = False,
+)
+
+java_fuzz_target_test(
+ name = "ScriptEngineInjection",
+ srcs = [
+ "ScriptEngineInjection.java",
+ ],
+ allowed_findings = [
+ "com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical",
+ ],
+ target_class = "com.example.ScriptEngineInjection",
+ verify_crash_reproducer = False,
+)
diff --git a/sanitizers/src/test/java/com/example/ClassLoaderLoadClass.java b/sanitizers/src/test/java/com/example/ClassLoaderLoadClass.java
index c3fa47ac..207f29cd 100644
--- a/sanitizers/src/test/java/com/example/ClassLoaderLoadClass.java
+++ b/sanitizers/src/test/java/com/example/ClassLoaderLoadClass.java
@@ -22,9 +22,11 @@ public class ClassLoaderLoadClass {
String input = data.consumeRemainingAsAsciiString();
try {
// create an instance to trigger class initialization
- ClassLoaderLoadClass.class.getClassLoader().loadClass(input).getConstructor().newInstance();
- } catch (ClassNotFoundException | InvocationTargetException | InstantiationException
- | IllegalAccessException | NoSuchMethodException ignored) {
+ ClassLoaderLoadClass.class.getClassLoader().loadClass(input).newInstance();
+ // TODO(khaled): this fails to reproduce the finding. It seems that this is related to not
+ // throwing a hard-to-catch error when not running in the fuzzing mode.
+ // ClassLoaderLoadClass.class.getClassLoader().loadClass(input).getConstructor().newInstance();
+ } catch (ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
}
}
}
diff --git a/sanitizers/src/test/java/com/example/DisabledHooksTest.java b/sanitizers/src/test/java/com/example/DisabledHooksTest.java
new file mode 100644
index 00000000..763cd637
--- /dev/null
+++ b/sanitizers/src/test/java/com/example/DisabledHooksTest.java
@@ -0,0 +1,110 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.example;
+
+import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh;
+import java.io.*;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Base64;
+import org.junit.After;
+import org.junit.Test;
+
+public class DisabledHooksTest {
+ public static void triggerReflectiveCallSanitizer() {
+ try {
+ Class.forName("jaz.Zer").newInstance();
+ } catch (ClassNotFoundException | IllegalAccessException | InstantiationException ignored) {
+ }
+ }
+
+ public static void triggerExpressionLanguageInjectionSanitizer() throws Throwable {
+ try {
+ Class.forName("jaz.Zer").getMethod("el").invoke(null);
+ } catch (InvocationTargetException e) {
+ throw e.getCause();
+ } catch (IllegalAccessException | ClassNotFoundException | NoSuchMethodException ignore) {
+ }
+ }
+
+ public static void triggerDeserializationSanitizer() {
+ byte[] data =
+ Base64.getDecoder().decode("rO0ABXNyAAdqYXouWmVyAAAAAAAAACoCAAFCAAlzYW5pdGl6ZXJ4cAEK");
+ try {
+ ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
+ System.out.println(ois.readObject());
+ } catch (IOException | ClassNotFoundException ignore) {
+ }
+ }
+
+ @After
+ public void resetDisabledHooksProperty() {
+ System.clearProperty("jazzer.disabled_hooks");
+ }
+
+ @Test(expected = FuzzerSecurityIssueHigh.class)
+ public void enableReflectiveCallSanitizer() {
+ triggerReflectiveCallSanitizer();
+ }
+
+ @Test(expected = FuzzerSecurityIssueHigh.class)
+ public void enableDeserializationSanitizer() {
+ triggerDeserializationSanitizer();
+ }
+
+ @Test(expected = FuzzerSecurityIssueHigh.class)
+ public void enableExpressionLanguageInjectionSanitizer() throws Throwable {
+ triggerExpressionLanguageInjectionSanitizer();
+ }
+
+ @Test
+ public void disableReflectiveCallSanitizer() {
+ System.setProperty(
+ "jazzer.disabled_hooks", "com.code_intelligence.jazzer.sanitizers.ReflectiveCall");
+ triggerReflectiveCallSanitizer();
+ }
+
+ @Test
+ public void disableDeserializationSanitizer() {
+ System.setProperty(
+ "jazzer.disabled_hooks", "com.code_intelligence.jazzer.sanitizers.Deserialization");
+ triggerDeserializationSanitizer();
+ }
+
+ @Test
+ public void disableExpressionLanguageSanitizer() throws Throwable {
+ System.setProperty("jazzer.disabled_hooks",
+ "com.code_intelligence.jazzer.sanitizers.ExpressionLanguageInjection");
+ triggerExpressionLanguageInjectionSanitizer();
+ }
+
+ @Test(expected = FuzzerSecurityIssueHigh.class)
+ public void disableReflectiveCallAndEnableDeserialization() {
+ System.setProperty(
+ "jazzer.disabled_hooks", "com.code_intelligence.jazzer.sanitizers.ReflectiveCall");
+ triggerReflectiveCallSanitizer();
+ triggerDeserializationSanitizer();
+ }
+
+ @Test
+ public void disableAllSanitizers() throws Throwable {
+ System.setProperty("jazzer.disabled_hooks",
+ "com.code_intelligence.jazzer.sanitizers.ReflectiveCall,"
+ + "com.code_intelligence.jazzer.sanitizers.Deserialization,"
+ + "com.code_intelligence.jazzer.sanitizers.ExpressionLanguageInjection");
+ triggerReflectiveCallSanitizer();
+ triggerExpressionLanguageInjectionSanitizer();
+ triggerDeserializationSanitizer();
+ }
+}
diff --git a/sanitizers/src/test/java/com/example/ExpressionLanguageInjection.java b/sanitizers/src/test/java/com/example/ExpressionLanguageInjection.java
index e26a9117..7d0192ab 100644
--- a/sanitizers/src/test/java/com/example/ExpressionLanguageInjection.java
+++ b/sanitizers/src/test/java/com/example/ExpressionLanguageInjection.java
@@ -15,33 +15,20 @@
package com.example;
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-import javax.validation.*;
-
-class UserData {
- public UserData(String email) {
- this.email = email;
- }
-
- @ValidEmailConstraint private String email;
-}
-
-@Constraint(validatedBy = InsecureEmailValidator.class)
-@Target({ElementType.METHOD, ElementType.FIELD})
-@Retention(RetentionPolicy.RUNTIME)
-@interface ValidEmailConstraint {
- String message() default "Invalid email address";
- Class<?>[] groups() default {};
- Class<? extends Payload>[] payload() default {};
-}
+import com.example.el.UserData;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import javax.validation.Validation;
+import javax.validation.Validator;
public class ExpressionLanguageInjection {
final private static Validator validator =
Validation.buildDefaultValidatorFactory().getValidator();
+ public static void fuzzerInitialize() {
+ LogManager.getLogManager().getLogger("").setLevel(Level.SEVERE);
+ }
+
public static void fuzzerTestOneInput(FuzzedDataProvider data) {
UserData uncheckedUserData = new UserData(data.consumeRemainingAsString());
validator.validate(uncheckedUserData);
diff --git a/sanitizers/src/test/java/com/example/LdapDnInjection.java b/sanitizers/src/test/java/com/example/LdapDnInjection.java
index 911db1dc..2fdf4a0c 100644
--- a/sanitizers/src/test/java/com/example/LdapDnInjection.java
+++ b/sanitizers/src/test/java/com/example/LdapDnInjection.java
@@ -15,20 +15,13 @@
package com.example;
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
-import java.util.Hashtable;
-import javax.naming.Context;
-import javax.naming.NamingException;
-import javax.naming.directory.InitialDirContext;
+import com.example.ldap.MockLdapContext;
+import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
+@SuppressWarnings("BanJNDI")
public class LdapDnInjection {
- private static InitialDirContext ctx;
-
- public static void fuzzerInitialize() throws NamingException {
- Hashtable<String, String> env = new Hashtable<>();
- env.put(Context.INITIAL_CONTEXT_FACTORY, "com.example.ldap.MockInitialContextFactory");
- ctx = new InitialDirContext(env);
- }
+ private static final DirContext ctx = new MockLdapContext();
public static void fuzzerTestOneInput(FuzzedDataProvider fuzzedDataProvider) throws Exception {
// Externally provided DN input needs to be escaped properly
diff --git a/sanitizers/src/test/java/com/example/LdapSearchInjection.java b/sanitizers/src/test/java/com/example/LdapSearchInjection.java
index b3dfee74..4ac84931 100644
--- a/sanitizers/src/test/java/com/example/LdapSearchInjection.java
+++ b/sanitizers/src/test/java/com/example/LdapSearchInjection.java
@@ -15,20 +15,13 @@
package com.example;
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
-import java.util.Hashtable;
-import javax.naming.Context;
-import javax.naming.NamingException;
+import com.example.ldap.MockLdapContext;
import javax.naming.directory.SearchControls;
-import javax.naming.ldap.InitialLdapContext;
+import javax.naming.ldap.LdapContext;
+@SuppressWarnings("BanJNDI")
public class LdapSearchInjection {
- private static InitialLdapContext ctx;
-
- public static void fuzzerInitialize() throws NamingException {
- Hashtable<String, String> env = new Hashtable<>();
- env.put(Context.INITIAL_CONTEXT_FACTORY, "com.example.ldap.MockInitialContextFactory");
- ctx = new InitialLdapContext(env, null);
- }
+ private static final LdapContext ctx = new MockLdapContext();
public static void fuzzerTestOneInput(FuzzedDataProvider fuzzedDataProvider) throws Exception {
// Externally provided LDAP query input needs to be escaped properly
diff --git a/sanitizers/src/test/java/com/example/ReflectiveCall.java b/sanitizers/src/test/java/com/example/ReflectiveCall.java
index e6b62b45..d7b3e46c 100644
--- a/sanitizers/src/test/java/com/example/ReflectiveCall.java
+++ b/sanitizers/src/test/java/com/example/ReflectiveCall.java
@@ -22,8 +22,8 @@ public class ReflectiveCall {
if (input.startsWith("@")) {
String className = input.substring(1);
try {
- Class.forName(className);
- } catch (ClassNotFoundException ignored) {
+ Class.forName(className).newInstance();
+ } catch (ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
}
}
}
diff --git a/sanitizers/src/test/java/com/example/ScriptEngineInjection.java b/sanitizers/src/test/java/com/example/ScriptEngineInjection.java
new file mode 100644
index 00000000..631b7ab8
--- /dev/null
+++ b/sanitizers/src/test/java/com/example/ScriptEngineInjection.java
@@ -0,0 +1,171 @@
+// Copyright 2023 Code Intelligence GmbH
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.example;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.Writer;
+import java.util.List;
+import javax.script.Bindings;
+import javax.script.ScriptContext;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineFactory;
+
+public class ScriptEngineInjection {
+ private final static ScriptEngine engine = new DummyScriptEngine();
+ private final static ScriptContext context = new DummyScriptContext();
+
+ private static void insecureScriptEval(String input) throws Exception {
+ engine.eval(new StringReader(input), context);
+ }
+
+ public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Exception {
+ try {
+ insecureScriptEval(data.consumeRemainingAsAsciiString());
+ } catch (Exception ignored) {
+ }
+ }
+
+ private static class DummyScriptEngine implements ScriptEngine {
+ @Override
+ public Bindings createBindings() {
+ return null;
+ }
+
+ @Override
+ public Object eval(String script) {
+ return null;
+ }
+
+ @Override
+ public Object eval(Reader reader) {
+ return null;
+ }
+
+ @Override
+ public Object eval(String script, ScriptContext context) {
+ return null;
+ }
+
+ @Override
+ public Object eval(Reader reader, ScriptContext context) {
+ return null;
+ }
+
+ @Override
+ public Object eval(String script, Bindings n) {
+ return null;
+ }
+
+ @Override
+ public Object eval(Reader reader, Bindings n) {
+ return null;
+ }
+
+ @Override
+ public Object get(String key) {
+ return null;
+ }
+
+ @Override
+ public Bindings getBindings(int scope) {
+ return null;
+ }
+
+ @Override
+ public ScriptContext getContext() {
+ return null;
+ }
+
+ @Override
+ public ScriptEngineFactory getFactory() {
+ return null;
+ }
+
+ @Override
+ public void put(String key, Object value) {}
+
+ @Override
+ public void setBindings(Bindings bindings, int scope) {}
+
+ @Override
+ public void setContext(ScriptContext context) {}
+
+ public DummyScriptEngine() {}
+ }
+
+ private static class DummyScriptContext implements ScriptContext {
+ @Override
+ public void setBindings(Bindings bindings, int scope) {}
+
+ @Override
+ public Bindings getBindings(int scope) {
+ return null;
+ }
+
+ @Override
+ public void setAttribute(String name, Object value, int scope) {}
+
+ @Override
+ public Object getAttribute(String name, int scope) {
+ return null;
+ }
+
+ @Override
+ public Object removeAttribute(String name, int scope) {
+ return null;
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ return null;
+ }
+
+ @Override
+ public int getAttributesScope(String name) {
+ return 0;
+ }
+
+ @Override
+ public Writer getWriter() {
+ return null;
+ }
+
+ @Override
+ public Writer getErrorWriter() {
+ return null;
+ }
+
+ @Override
+ public void setWriter(Writer writer) {}
+
+ @Override
+ public void setErrorWriter(Writer writer) {}
+
+ @Override
+ public Reader getReader() {
+ return null;
+ }
+
+ @Override
+ public void setReader(Reader reader) {}
+
+ @Override
+ public List<Integer> getScopes() {
+ return null;
+ }
+ }
+}
diff --git a/sanitizers/src/test/java/com/example/SsrfHttpClient.java b/sanitizers/src/test/java/com/example/SsrfHttpClient.java
new file mode 100644
index 00000000..6da561a9
--- /dev/null
+++ b/sanitizers/src/test/java/com/example/SsrfHttpClient.java
@@ -0,0 +1,39 @@
+// Copyright 2023 Code Intelligence GmbH
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.example;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import java.io.IOException;
+import java.net.*;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+public class SsrfHttpClient {
+ private static final HttpClient CLIENT =
+ HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
+
+ public static void fuzzerTestOneInput(FuzzedDataProvider data)
+ throws IOException, InterruptedException {
+ String hostname = data.consumeString(15);
+ URI uri;
+ try {
+ uri = URI.create("https://" + hostname);
+ HttpRequest request = HttpRequest.newBuilder().uri(uri).GET().build();
+ CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
+ } catch (IllegalArgumentException ignored) {
+ }
+ }
+}
diff --git a/sanitizers/src/test/java/com/example/ldap/MockInitialContextFactory.java b/sanitizers/src/test/java/com/example/SsrfSocketConnect.java
index b674f5c5..f1d7a59b 100644
--- a/sanitizers/src/test/java/com/example/ldap/MockInitialContextFactory.java
+++ b/sanitizers/src/test/java/com/example/SsrfSocketConnect.java
@@ -1,4 +1,4 @@
-// Copyright 2021 Code Intelligence GmbH
+// Copyright 2023 Code Intelligence GmbH
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,15 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.example.ldap;
+package com.example;
-import java.util.Hashtable;
-import javax.naming.Context;
-import javax.naming.NamingException;
-import javax.naming.spi.InitialContextFactory;
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import java.net.Socket;
-public class MockInitialContextFactory implements InitialContextFactory {
- public Context getInitialContext(Hashtable environment) {
- return new MockLdapContext();
+public class SsrfSocketConnect {
+ public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Exception {
+ String hostname = data.consumeString(15);
+ try (Socket s = new Socket(hostname, 80)) {
+ s.getInetAddress();
+ }
}
}
diff --git a/sanitizers/src/test/java/com/example/SsrfSocketConnectToHost.java b/sanitizers/src/test/java/com/example/SsrfSocketConnectToHost.java
new file mode 100644
index 00000000..3e60e503
--- /dev/null
+++ b/sanitizers/src/test/java/com/example/SsrfSocketConnectToHost.java
@@ -0,0 +1,46 @@
+// Copyright 2023 Code Intelligence GmbH
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.example;
+
+import com.code_intelligence.jazzer.api.BugDetectors;
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.api.Jazzer;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+
+public class SsrfSocketConnectToHost {
+ // We don't actually care about establishing a connection and thus choose the lowest possible
+ // timeout.
+ private static final int CONNECTION_TIMEOUT_MS = 1;
+
+ public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Exception {
+ String host = data.consumeAsciiString(15);
+ int port = data.consumeInt(1, 65535);
+
+ try (AutoCloseable ignored = BugDetectors.allowNetworkConnections()) {
+ // Verify that policies nest properly.
+ try (AutoCloseable ignored1 = BugDetectors.allowNetworkConnections(
+ (String h, Integer p) -> h.equals("localhost"))) {
+ try (AutoCloseable ignored2 = BugDetectors.allowNetworkConnections()) {
+ }
+ try (Socket s = new Socket()) {
+ s.connect(new InetSocketAddress(host, port), CONNECTION_TIMEOUT_MS);
+ } catch (IOException ignored3) {
+ }
+ }
+ }
+ }
+}
diff --git a/sanitizers/src/test/java/com/example/SsrfUrlConnection.java b/sanitizers/src/test/java/com/example/SsrfUrlConnection.java
new file mode 100644
index 00000000..8ea940a1
--- /dev/null
+++ b/sanitizers/src/test/java/com/example/SsrfUrlConnection.java
@@ -0,0 +1,33 @@
+// Copyright 2023 Code Intelligence GmbH
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.example;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class SsrfUrlConnection {
+ public static void fuzzerTestOneInput(FuzzedDataProvider data) throws Exception {
+ String hostname = data.consumeString(15);
+ try {
+ URL url = new URL("https://" + hostname);
+ HttpURLConnection con = (HttpURLConnection) url.openConnection();
+ con.setRequestMethod("GET");
+ con.getInputStream();
+ } catch (IOException | IllegalArgumentException ignored) {
+ }
+ }
+}
diff --git a/sanitizers/src/test/java/com/example/StackOverflowRegexInjection.java b/sanitizers/src/test/java/com/example/StackOverflowRegexInjection.java
new file mode 100644
index 00000000..92dfcf38
--- /dev/null
+++ b/sanitizers/src/test/java/com/example/StackOverflowRegexInjection.java
@@ -0,0 +1,51 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.example;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import java.util.regex.Pattern;
+
+/**
+ * Compiling a regex pattern can lead to stack overflows and thus is caught
+ * in the constructor of {@link java.util.regex.Pattern} and rethrown as a
+ * {@link java.util.regex.PatternSyntaxException}.
+ * The {@link com.code_intelligence.jazzer.sanitizers.RegexInjection} sanitizer
+ * uses this exception to detect injections and would incorrectly report a
+ * finding. Exceptions caused by stack overflows should not be handled in the
+ * hook as it's very unlikely that the fuzzer generates a pattern causing a
+ * stack overflow before it generates an invalid one.
+ */
+@SuppressWarnings({"ReplaceOnLiteralHasNoEffect", "ResultOfMethodCallIgnored"})
+public class StackOverflowRegexInjection {
+ public static void fuzzerTestOneInput(FuzzedDataProvider ignored) {
+ // load regex classes by using them beforehand,
+ // otherwise initialization would cause other issues.
+ Pattern.compile("\n").matcher("some string").replaceAll("\\\\n");
+
+ generatePatternSyntaxException();
+ }
+
+ @SuppressWarnings("InfiniteRecursion")
+ private static void generatePatternSyntaxException() {
+ // try-catch on every level to not unwind the stack
+ try {
+ // generate stack overflow
+ generatePatternSyntaxException();
+ } catch (StackOverflowError e) {
+ // invoke regex injection hook
+ "some sting".replaceAll("\n", "\\\\n");
+ }
+ }
+}
diff --git a/sanitizers/src/test/java/com/example/XPathInjection.java b/sanitizers/src/test/java/com/example/XPathInjection.java
new file mode 100644
index 00000000..e8fe22a0
--- /dev/null
+++ b/sanitizers/src/test/java/com/example/XPathInjection.java
@@ -0,0 +1,53 @@
+// Copyright 2022 Code Intelligence GmbH
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.example;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import java.io.*;
+import javax.xml.parsers.*;
+import javax.xml.xpath.*;
+import org.w3c.dom.Document;
+import org.xml.sax.*;
+
+public class XPathInjection {
+ static Document doc = null;
+ static XPath xpath = null;
+
+ public static void fuzzerInitialize() throws Exception {
+ String xmlFile = "<user name=\"user\" pass=\"pass\"></user>";
+
+ DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
+ domFactory.setNamespaceAware(true);
+ DocumentBuilder builder = domFactory.newDocumentBuilder();
+ doc = builder.parse(new InputSource(new StringReader(xmlFile)));
+
+ XPathFactory xpathFactory = XPathFactory.newInstance();
+ xpath = xpathFactory.newXPath();
+ }
+
+ public static void unsafeEval(String user, String pass) {
+ if (user != null && pass != null) {
+ String expression = "/user[@name='" + user + "' and @pass='" + pass + "']";
+ try {
+ xpath.evaluate(expression, doc, XPathConstants.BOOLEAN);
+ } catch (XPathExpressionException e) {
+ }
+ }
+ }
+
+ public static void fuzzerTestOneInput(FuzzedDataProvider data) {
+ unsafeEval(data.consumeString(20), data.consumeRemainingAsString());
+ }
+}
diff --git a/sanitizers/src/test/java/com/example/el/BUILD.bazel b/sanitizers/src/test/java/com/example/el/BUILD.bazel
new file mode 100644
index 00000000..bf12a48b
--- /dev/null
+++ b/sanitizers/src/test/java/com/example/el/BUILD.bazel
@@ -0,0 +1,15 @@
+java_library(
+ name = "ExpressionLanguageExample",
+ srcs = [
+ "InsecureEmailValidator.java",
+ "UserData.java",
+ ],
+ visibility = ["//sanitizers/src/test/java/com/example:__pkg__"],
+ deps = [
+ "@maven//:javax_el_javax_el_api",
+ "@maven//:javax_validation_validation_api",
+ "@maven//:javax_xml_bind_jaxb_api",
+ "@maven//:org_glassfish_javax_el",
+ "@maven//:org_hibernate_hibernate_validator",
+ ],
+)
diff --git a/sanitizers/src/test/java/com/example/InsecureEmailValidator.java b/sanitizers/src/test/java/com/example/el/InsecureEmailValidator.java
index d61e888d..e10b082e 100644
--- a/sanitizers/src/test/java/com/example/InsecureEmailValidator.java
+++ b/sanitizers/src/test/java/com/example/el/InsecureEmailValidator.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.example;
+package com.example.el;
import static java.lang.String.format;
diff --git a/sanitizers/src/test/java/com/example/el/UserData.java b/sanitizers/src/test/java/com/example/el/UserData.java
new file mode 100644
index 00000000..305e78ee
--- /dev/null
+++ b/sanitizers/src/test/java/com/example/el/UserData.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 Code Intelligence GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.el;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import javax.validation.Constraint;
+import javax.validation.Payload;
+
+public class UserData {
+ public UserData(String email) {
+ this.email = email;
+ }
+
+ @ValidEmailConstraint private String email;
+}
+
+@Constraint(validatedBy = InsecureEmailValidator.class)
+@Target({ElementType.METHOD, ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@interface ValidEmailConstraint {
+ String message() default "Invalid email address";
+ Class<?>[] groups() default {};
+ Class<? extends Payload>[] payload() default {};
+}