diff --git a/MODULE.bazel b/MODULE.bazel index 382f5531e..78474a667 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -88,6 +88,7 @@ TEST_MAVEN_ARTIFACTS = [ "ognl:ognl:3.3.0", "org.apache.commons:commons-jexl:2.1.1", "org.assertj:assertj-core:3.27.6", + "org.freemarker:freemarker:2.3.34", "org.jacoco:org.jacoco.core:0.8.14", "org.mockito:mockito-core:5.20.0", "org.mvel:mvel2:2.5.2.Final", diff --git a/maven_install.json b/maven_install.json index fef74579a..bf222ac1d 100755 --- a/maven_install.json +++ b/maven_install.json @@ -1,7 +1,7 @@ { "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL", - "__INPUT_ARTIFACTS_HASH": -620116506, - "__RESOLVED_ARTIFACTS_HASH": 1157173568, + "__INPUT_ARTIFACTS_HASH": -1980670473, + "__RESOLVED_ARTIFACTS_HASH": -157334146, "conflict_resolution": { "com.google.code.gson:gson:2.8.6": "com.google.code.gson:gson:2.8.9", "com.google.j2objc:j2objc-annotations:2.8": "com.google.j2objc:j2objc-annotations:3.1", @@ -428,6 +428,12 @@ }, "version": "3.27.6" }, + "org.freemarker:freemarker": { + "shasums": { + "jar": "9a9fb91cd64199232eb1ca9766148a5d30ef8944be5fac051018f96c70c8f6a3" + }, + "version": "2.3.34" + }, "org.glassfish:javax.el": { "shasums": { "jar": "c255fe3ff4d7e491caf92c10c497f3c77d19acc4832d9bd2e80180d168fcedd2" @@ -1854,6 +1860,27 @@ "org.assertj.core.util.introspection", "org.assertj.core.util.xml" ], + "org.freemarker:freemarker": [ + "freemarker.cache", + "freemarker.core", + "freemarker.debug", + "freemarker.debug.impl", + "freemarker.ext.ant", + "freemarker.ext.beans", + "freemarker.ext.dom", + "freemarker.ext.jakarta.jsp", + "freemarker.ext.jakarta.servlet", + "freemarker.ext.jdom", + "freemarker.ext.jsp", + "freemarker.ext.jython", + "freemarker.ext.rhino", + "freemarker.ext.servlet", + "freemarker.ext.util", + "freemarker.ext.xml", + "freemarker.log", + "freemarker.template", + "freemarker.template.utility" + ], "org.glassfish:javax.el": [ "com.sun.el", "com.sun.el.lang", @@ -2840,6 +2867,7 @@ "org.apache.xmlgraphics:xmlgraphics-commons", "org.apiguardian:apiguardian-api", "org.assertj:assertj-core", + "org.freemarker:freemarker", "org.glassfish:javax.el", "org.hamcrest:hamcrest-core", "org.hibernate:hibernate-validator", diff --git a/sanitizers/sanitizers.bzl b/sanitizers/sanitizers.bzl index 9f32fba30..88b04be64 100644 --- a/sanitizers/sanitizers.bzl +++ b/sanitizers/sanitizers.bzl @@ -32,6 +32,7 @@ _sanitizer_class_names = [ "ScriptEngineInjection", "ServerSideRequestForgery", "SqlInjection", + "TemplateInjection", "UnsafeSanitizer", "XPathInjection", "XmlParserSsrfGuidance", 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 89fb76664..59dcf55b9 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 @@ -79,6 +79,7 @@ kt_jvm_library( "OsCommandInjection.kt", "ReflectiveCall.kt", "RegexInjection.kt", + "TemplateInjection.kt", "Utils.kt", "XPathInjection.kt", "XmlParserSsrfGuidance.kt", diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/TemplateInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/TemplateInjection.kt new file mode 100644 index 000000000..25ff0a91f --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/TemplateInjection.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2025 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.HookType +import com.code_intelligence.jazzer.api.Jazzer +import com.code_intelligence.jazzer.api.MethodHook +import java.io.Reader +import java.lang.invoke.MethodHandle + +/** + * Guides FreeMarker templates towards patterns that can trigger OS command injections. + * + * This does not report findings directly; it steers inputs towards OS command injections, + * which are detected by another bug detector. + */ +@Suppress("unused_parameter", "unused") +object TemplateInjection { + private const val FREEMARKER_INJECTION_ATTACK: String = "\${\"freemarker.template.utility.Execute\"?new()(\"jazze\")}" + + init { + require(FREEMARKER_INJECTION_ATTACK.length <= 64) { + "FreeMarker injection must fit in a table of recent compares entry (64 bytes)" + } + } + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "freemarker.template.Template", + targetMethod = "", + ) + @JvmStatic + fun hookFreemarker( + method: MethodHandle?, + thisObject: Any?, + arguments: Array, + hookId: Int, + ) { + (1..2) + .mapNotNull { idx -> (arguments.getOrNull(idx) as? Reader) } + .forEach { reader -> + Jazzer.guideTowardsContainment( + peekMarkableReader(reader, FREEMARKER_INJECTION_ATTACK.length), + FREEMARKER_INJECTION_ATTACK, + hookId, + ) + } + } +} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/XmlParserSsrfGuidance.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/XmlParserSsrfGuidance.kt index 65607e26b..6846d9778 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/XmlParserSsrfGuidance.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/XmlParserSsrfGuidance.kt @@ -31,7 +31,7 @@ import java.lang.invoke.MethodHandle * (e.g. Socket/SocketChannel hooks) can observe network connections initiated by XML parsers * resolving external entities, schemas, or includes. */ -@Suppress("unused") +@Suppress("unused_parameter", "unused") object XmlParserSsrfGuidance { private val EXTERNAL_DOCTYPE = "" private val EXTERNAL_DOCTYPE_SIZE = EXTERNAL_DOCTYPE.toByteArray().size diff --git a/sanitizers/src/test/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel b/sanitizers/src/test/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel index a9bc80797..3de4f58a0 100644 --- a/sanitizers/src/test/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel +++ b/sanitizers/src/test/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel @@ -26,6 +26,7 @@ java_junit5_test( "@maven//:javax_validation_validation_api", "@maven//:ognl_ognl", "@maven//:org_apache_commons_commons_jexl", + "@maven//:org_freemarker_freemarker", "@maven//:org_junit_jupiter_junit_jupiter_api", "@maven//:org_junit_jupiter_junit_jupiter_params", "@maven//:org_mvel_mvel2", diff --git a/sanitizers/src/test/java/com/example/BUILD.bazel b/sanitizers/src/test/java/com/example/BUILD.bazel index 038080a63..4d64b0fca 100644 --- a/sanitizers/src/test/java/com/example/BUILD.bazel +++ b/sanitizers/src/test/java/com/example/BUILD.bazel @@ -314,6 +314,26 @@ java_fuzz_target_test( ], ) +java_fuzz_target_test( + name = "TemplateInjection", + srcs = [ + "TemplateInjection.java", + ], + allowed_findings = [ + "com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical", + ], + # FreeMarker has too many classes to instrument safely + fuzzer_args = [ + "--instrumentation_includes=com.example.**", + ], + tags = ["dangerous"], + target_class = "com.example.TemplateInjection", + verify_crash_reproducer = False, + deps = [ + "@maven//:org_freemarker_freemarker", + ], +) + [java_fuzz_target_test( name = "UnsafeArrayOutOfBounds_" + method, srcs = [ diff --git a/sanitizers/src/test/java/com/example/TemplateInjection.java b/sanitizers/src/test/java/com/example/TemplateInjection.java new file mode 100644 index 000000000..909d1becf --- /dev/null +++ b/sanitizers/src/test/java/com/example/TemplateInjection.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 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 freemarker.template.Template; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; + +public class TemplateInjection { + public static void fuzzerTestOneInput(FuzzedDataProvider fdp) throws Exception { + Map model = new HashMap<>(); + String data = fdp.consumeRemainingAsString(); + try { + Template tmpl = new Template("test", new StringReader(data)); + tmpl.process(model, new StringWriter()); + } catch (Throwable ignored) { + } + } +}