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