diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/Freemarker.qll b/java/ql/src/experimental/Security/CWE/CWE-094/Freemarker.qll
new file mode 100644
index 000000000000..e572bfa1d4ea
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-094/Freemarker.qll
@@ -0,0 +1,195 @@
+/**
+ * This library models Apache FreeMarker template engine
+ */
+
+import java
+import semmle.code.java.dataflow.DataFlow2
+import semmle.code.java.dataflow.DataFlow3
+
+module Freemarker {
+ class FreemarkerConfiguration extends RefType {
+ FreemarkerConfiguration() { this.hasQualifiedName("freemarker.core", "Configurable") }
+ }
+
+ class TemplateClassResolver extends RefType {
+ TemplateClassResolver() { this.hasQualifiedName("freemarker.core", "TemplateClassResolver") }
+ }
+
+ class FreemarkerTemplate extends RefType {
+ FreemarkerTemplate() { this.hasQualifiedName("freemarker.template", "Template") }
+ }
+
+ // https://github.com/sanluan/PublicCMS/blob/d617de930d78e5ca17357614c1209ce410eae403/publiccms-parent/publiccms-core/src/main/java/com/publiccms/logic/component/site/DirectiveComponent.java
+ // https://github.com/Rekoe/Rk_Cms/blob/999854b156e4d7c8627095066e8f80f053645528/src/main/java/com/rekoe/service/FileService.java
+ class FreeMarkerConfigurer extends RefType {
+ FreeMarkerConfigurer() {
+ exists(string package |
+ this.hasQualifiedName(package, "FreeMarkerConfigurer") and
+ package.matches("%freemarker%")
+ )
+ }
+ }
+
+ // https://github.com/hibernate/hibernate-tools/blob/71fa3dae6ac1ac1b9c59f4fef5ba056c5ac36b34/orm/src/main/java/org/hibernate/tool/internal/export/common/TemplateHelper.java
+ class FreemarkerTemplateConfiguration extends RefType {
+ FreemarkerTemplateConfiguration() {
+ this.hasQualifiedName("freemarker.template", "Configuration")
+ }
+ }
+
+ class FreemarkerStringTemplateLoader extends RefType {
+ FreemarkerStringTemplateLoader() {
+ this.hasQualifiedName("freemarker.cache", "StringTemplateLoader")
+ }
+ }
+
+ Expr getAllowNothingResolverExpr() {
+ exists(Field f |
+ result = f.getAnAccess() and
+ f.hasName("ALLOWS_NOTHING_RESOLVER") and
+ f.getDeclaringType() instanceof TemplateClassResolver
+ )
+ }
+
+ class FreemarkerSetClassResolver extends MethodAccess {
+ FreemarkerSetClassResolver() {
+ exists(Method m |
+ m = this.getMethod() and
+ m.getDeclaringType() instanceof FreemarkerConfiguration and
+ m.hasName("setNewBuiltinClassResolver") and
+ m.getAParameter().getAnArgument() = getAllowNothingResolverExpr()
+ )
+ }
+ }
+
+ // setSetting method configuration method: https://freemarker.apache.org/docs/api/freemarker/template/Configuration.html#setSetting-java.lang.String-java.lang.String-
+ class FreemarkerSetSettingClassResolver extends MethodAccess {
+ FreemarkerSetSettingClassResolver() {
+ exists(Method m, string s |
+ m = this.getMethod() and
+ m.getDeclaringType() instanceof FreemarkerTemplateConfiguration and
+ m.hasName("setSetting") and
+ m.getParameter(0).getAnArgument().(Literal).getValue().matches("new_builtin_class_resolver") and
+ m.getParameter(1).getAnArgument().(Literal).getValue().matches(s) and
+ s in ["allows_nothing", "allowsNothing"]
+ )
+ }
+ }
+
+ class FreemarkerSetAPIBuiltinEnabled extends MethodAccess {
+ FreemarkerSetAPIBuiltinEnabled() {
+ exists(Method m |
+ m = this.getMethod() and
+ m.getDeclaringType() instanceof FreemarkerConfiguration and
+ m.hasName("setAPIBuiltinEnabled") and
+ m.getAParameter().getAnArgument().(BooleanLiteral).getBooleanValue() = true
+ )
+ }
+ }
+
+ class GetConfigurationCall extends MethodAccess {
+ GetConfigurationCall() {
+ exists(Method m |
+ m = this.getMethod() and
+ m.getDeclaringType() instanceof FreeMarkerConfigurer and
+ m.hasName("getConfiguration")
+ )
+ }
+ }
+
+ class SafeFreemarkerConfiguration extends DataFlow2::Configuration {
+ SafeFreemarkerConfiguration() { this = "SafeFreemarkerConfiguration" }
+
+ override predicate isSource(DataFlow2::Node src) {
+ src.asExpr() instanceof FreemarkerTemplateConfigurationSource
+ }
+
+ override predicate isSink(DataFlow2::Node sink) {
+ sink.asExpr() = any(FreemarkerSetClassResolver r).getQualifier()
+ or
+ sink.asExpr() = any(FreemarkerSetSettingClassResolver r).getQualifier()
+ }
+ // override int fieldFlowBranchLimit() { result = 0 }
+ }
+
+ class UnsafeFreemarkerConfiguration extends DataFlow3::Configuration {
+ UnsafeFreemarkerConfiguration() { this = "UnsafeFreemarkerConfiguration" }
+
+ override predicate isSource(DataFlow3::Node src) {
+ src.asExpr() instanceof FreemarkerTemplateConfigurationSource
+ }
+
+ override predicate isSink(DataFlow3::Node sink) {
+ sink.asExpr() = any(FreemarkerSetAPIBuiltinEnabled r).getQualifier()
+ }
+ // override int fieldFlowBranchLimit() { result = 0 }
+ }
+
+ class FreemarkerTemplateConfigurationSource extends Expr {
+ FreemarkerTemplateConfigurationSource() {
+ this.(ClassInstanceExpr).getConstructedType() instanceof FreemarkerTemplateConfiguration
+ or
+ this instanceof GetConfigurationCall
+ }
+
+ predicate isSafe() {
+ exists(SafeFreemarkerConfiguration safeConfig |
+ safeConfig
+ .hasFlow(DataFlow2::exprNode(this),
+ DataFlow2::exprNode(any(FreemarkerSetClassResolver r).getQualifier()))
+ ) and
+ not exists(UnsafeFreemarkerConfiguration unsafeConfig |
+ unsafeConfig
+ .hasFlow(DataFlow3::exprNode(this),
+ DataFlow3::exprNode(any(FreemarkerSetAPIBuiltinEnabled r).getQualifier()))
+ )
+ }
+ }
+
+ /**
+ * Template created using new expr
+ * `Template t = new Template("name", templateStr, cfg);`
+ * ref: https://freemarker.apache.org/docs/api/index.html Class Template
+ */
+ class NewTemplate extends ClassInstanceExpr {
+ NewTemplate() { this.getConstructedType() instanceof FreemarkerTemplate }
+
+ predicate isReaderArg(int index) {
+ this.getConstructor()
+ .getParameter(index)
+ .getType()
+ .(RefType)
+ .hasQualifiedName("java.io", "Reader")
+ }
+
+ Expr getSink() {
+ // All constructors accept java.io.Reader as template source string.
+ exists(int index |
+ isReaderArg(index) and
+ result = this.getArgument(index)
+ )
+ or
+ // in one case constructor accepts java.lang.String as second arg instead of java.io.Reader
+ this.getNumArgument() = 3 and
+ not isReaderArg(1) and
+ result = this.getArgument(1)
+ }
+ }
+
+ /**
+ * Pass the template via StringTemplateLoader.
+ * `StringTemplateLoader stringLoader = new StringTemplateLoader();`
+ * `stringLoader.putTemplate("myTemplate", templateStr);`
+ */
+ class FreemarkerPutTemplate extends MethodAccess {
+ FreemarkerPutTemplate() {
+ exists(Method m |
+ m = this.getMethod() and
+ m.getDeclaringType() instanceof FreemarkerStringTemplateLoader and
+ m.hasName("putTemplate")
+ )
+ }
+
+ Expr getSink() { result = this.getArgument(1) }
+ }
+}
diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerTaintedTemplate.java b/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerTaintedTemplate.java
new file mode 100644
index 000000000000..389f213b9460
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerTaintedTemplate.java
@@ -0,0 +1,25 @@
+package com.example.freemarkertest;
+
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateExceptionHandler;
+import freemarker.template.Version;
+import freemarker.core.TemplateClassResolver;
+import freemarker.cache.StringTemplateLoader;
+
+{
+ Configuration cfg = new Configuration();
+ cfg.setDefaultEncoding("UTF-8");
+ cfg.setLocale(Locale.US);
+ cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
+
+ // cfg.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);
+ // cfg.setSetting("new_builtin_class_resolver", "allows_nothing");
+
+ // String templateStr="<#assign ex=\"freemarker.template.utility.Execute\"?new()> ${ex(\"id\")}";
+ String templateStr=argv[1];
+ Template t = new Template("name", new StringReader(templateStr), cfg);
+ Writer consoleWriter3 = new OutputStreamWriter(System.out);
+ t.process(input, consoleWriter3);
+
+}
\ No newline at end of file
diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerTaintedTemplate.qhelp b/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerTaintedTemplate.qhelp
new file mode 100644
index 000000000000..57c16eead7e7
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerTaintedTemplate.qhelp
@@ -0,0 +1,31 @@
+
+
+Template Injection occurs when user input is interpreted as template.
+When an attacker is able to use native template syntax to inject a malicious payload into a template,
+which is then executed server-side is results in Server Side Template Injection and Information Disclosure.
+
+To fix this, ensure that an untrusted value is not used as a template.
+
+Consider the example given below, an untrusted data is used to generate a template string.
+This can lead to remote code execution.
+Even if you disable class resolver it may lead to sensitive information disclosure through data model global variable.
+
${m.msg(title)}
+ + + diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerUnsafeConfiguration.java b/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerUnsafeConfiguration.java new file mode 100644 index 000000000000..dd3889ee7623 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerUnsafeConfiguration.java @@ -0,0 +1,29 @@ +package com.example.freemarkertest; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateExceptionHandler; +import freemarker.template.Version; +import freemarker.core.TemplateClassResolver; +import freemarker.cache.StringTemplateLoader; + +{ + Configuration cfg = new Configuration(); + cfg.setDirectoryForTemplateLoading(new File("/home/templates")); + + cfg.setDefaultEncoding("UTF-8"); + cfg.setLocale(Locale.US); + cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + + // cfg.setAPIBuiltinEnabled(true); + // cfg.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER); + // cfg.setSetting("new_builtin_class_resolver", "allows_nothing"); + + Map
+Apache FreeMarker is a template engine: a Java library to generate text output based on templates.
+Usually user input is passed throught key-value data model, and it is safe to use them.
+But sometimes developers allow to intepret user input as user directive with interpret filter function.
+It results in executing user-controlled data as part of template, which may lead up to remote code execution.
+
+It is generally recommended to avoid using interpret filter function.
+Additionally you should configure template engine by setting class resolver to ALLOWS_NOTHING_RESOLVER.
+
+Also there is method setAPIBuiltinEnabled which enables usage of builtin API.
+By default this property is set to false. And setting this value to true is unsafe.
+
+The following example pass untrusted data through data model, then interpret it as user definec directive and execute it. +
+