Skip to content

Commit 0bf5498

Browse files
committed
feat: add freemarker template injection guidance hook
1 parent 99ad906 commit 0bf5498

File tree

7 files changed

+148
-2
lines changed

7 files changed

+148
-2
lines changed

MODULE.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ TEST_MAVEN_ARTIFACTS = [
8181
"com.google.truth.extensions:truth-liteproto-extension:1.4.5",
8282
"com.google.truth.extensions:truth-proto-extension:1.4.5",
8383
"com.google.truth:truth:1.4.5",
84+
"freemarker:freemarker:2.3.1",
8485
"jakarta.el:jakarta.el-api:6.0.1",
8586
"jakarta.validation:jakarta.validation-api:3.0.2",
8687
"javax.persistence:javax.persistence-api:2.2",

maven_install.json

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
3-
"__INPUT_ARTIFACTS_HASH": -620116506,
4-
"__RESOLVED_ARTIFACTS_HASH": 1157173568,
3+
"__INPUT_ARTIFACTS_HASH": 962534745,
4+
"__RESOLVED_ARTIFACTS_HASH": -862033946,
55
"conflict_resolution": {
66
"com.google.code.gson:gson:2.8.6": "com.google.code.gson:gson:2.8.9",
77
"com.google.j2objc:j2objc-annotations:2.8": "com.google.j2objc:j2objc-annotations:3.1",
@@ -170,6 +170,12 @@
170170
},
171171
"version": "1.1.1"
172172
},
173+
"freemarker:freemarker": {
174+
"shasums": {
175+
"jar": "501cf529f069fbfb47b433043fa970b119472a04bda44ea8f94141ffb8ced6c0"
176+
},
177+
"version": "2.3.1"
178+
},
173179
"io.github.classgraph:classgraph": {
174180
"shasums": {
175181
"jar": "6e564e29cec95a392268a609f09071d56199383d906ac70e91753a7998d1a3e8"
@@ -1301,6 +1307,24 @@
13011307
"org.apache.commons.logging",
13021308
"org.apache.commons.logging.impl"
13031309
],
1310+
"freemarker:freemarker": [
1311+
"freemarker.cache",
1312+
"freemarker.core",
1313+
"freemarker.debug",
1314+
"freemarker.debug.impl",
1315+
"freemarker.ext.ant",
1316+
"freemarker.ext.beans",
1317+
"freemarker.ext.dom",
1318+
"freemarker.ext.jdom",
1319+
"freemarker.ext.jsp",
1320+
"freemarker.ext.jython",
1321+
"freemarker.ext.servlet",
1322+
"freemarker.ext.util",
1323+
"freemarker.ext.xml",
1324+
"freemarker.log",
1325+
"freemarker.template",
1326+
"freemarker.template.utility"
1327+
],
13041328
"io.github.classgraph:classgraph": [
13051329
"io.github.classgraph",
13061330
"nonapi.io.github.classgraph.classloaderhandler",
@@ -2797,6 +2821,7 @@
27972821
"com.unboundid:unboundid-ldapsdk",
27982822
"commons-io:commons-io",
27992823
"commons-logging:commons-logging",
2824+
"freemarker:freemarker",
28002825
"io.github.classgraph:classgraph",
28012826
"io.projectreactor:reactor-core",
28022827
"jakarta.el:jakarta.el-api",

sanitizers/sanitizers.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ _sanitizer_class_names = [
3232
"ScriptEngineInjection",
3333
"ServerSideRequestForgery",
3434
"SqlInjection",
35+
"TemplateInjection",
3536
"UnsafeSanitizer",
3637
"XPathInjection",
3738
"XmlParserSsrfGuidance",

sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ kt_jvm_library(
7979
"OsCommandInjection.kt",
8080
"ReflectiveCall.kt",
8181
"RegexInjection.kt",
82+
"TemplateInjection.kt",
8283
"Utils.kt",
8384
"XPathInjection.kt",
8485
"XmlParserSsrfGuidance.kt",
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2025 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.code_intelligence.jazzer.sanitizers
18+
19+
import com.code_intelligence.jazzer.api.HookType
20+
import com.code_intelligence.jazzer.api.Jazzer
21+
import com.code_intelligence.jazzer.api.MethodHook
22+
import java.io.Reader
23+
import java.lang.invoke.MethodHandle
24+
25+
/**
26+
* Guides FreeMarker templates towards patterns that can trigger OS command injections.
27+
*
28+
* This does not report findings directly; it steers inputs towards OS command injections,
29+
* which are detected by another bug detector.
30+
*/
31+
@Suppress("unused_parameter", "unused")
32+
object TemplateInjection {
33+
private const val FREEMARKER_INJECTION_ATTACK: String = "\${\"freemarker.template.utility.Execute\"?new()(\"jazze\")}"
34+
35+
init {
36+
require(FREEMARKER_INJECTION_ATTACK.length <= 64) {
37+
"Expression language exploit must fit in a table of recent compares entry (64 bytes)"
38+
}
39+
}
40+
41+
@MethodHook(
42+
type = HookType.BEFORE,
43+
targetClassName = "freemarker.template.Template",
44+
targetMethod = "<init>",
45+
)
46+
@JvmStatic
47+
fun hookFreemarker(
48+
method: MethodHandle?,
49+
thisObject: Any?,
50+
arguments: Array<Any>,
51+
hookId: Int,
52+
) {
53+
if (arguments.size < 2) return
54+
val reader = arguments[1] as? Reader ?: return
55+
Jazzer.guideTowardsContainment(
56+
peekMarkableReader(reader, FREEMARKER_INJECTION_ATTACK.length),
57+
FREEMARKER_INJECTION_ATTACK,
58+
hookId,
59+
)
60+
}
61+
}

sanitizers/src/test/java/com/example/BUILD.bazel

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,24 @@ java_fuzz_target_test(
314314
],
315315
)
316316

317+
java_fuzz_target_test(
318+
name = "TemplateInjection",
319+
srcs = [
320+
"TemplateInjection.java",
321+
],
322+
allowed_findings = [
323+
"com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical",
324+
],
325+
fuzzer_args = [
326+
"--instrumentation_includes=com.example.**",
327+
],
328+
target_class = "com.example.TemplateInjection",
329+
verify_crash_reproducer = False,
330+
deps = [
331+
"@maven//:freemarker_freemarker",
332+
],
333+
)
334+
317335
[java_fuzz_target_test(
318336
name = "UnsafeArrayOutOfBounds_" + method,
319337
srcs = [
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2025 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example;
18+
19+
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
20+
import freemarker.template.Template;
21+
import java.io.StringReader;
22+
import java.io.StringWriter;
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
26+
public class TemplateInjection {
27+
public static void fuzzerTestOneInput(FuzzedDataProvider fdp) throws Exception {
28+
Map<String, Object> model = new HashMap<>();
29+
String data = fdp.consumeRemainingAsString();
30+
if (data.isEmpty()) {
31+
return;
32+
}
33+
try {
34+
Template tmpl = new Template("test", new StringReader(data));
35+
tmpl.process(model, new StringWriter());
36+
} catch (Throwable ignored) {
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)