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. +

+ +
+ + +
  • Portswigger : [Server Side Template Injection](https://portswigger.net/web-security/server-side-template-injection)
  • +
    + +
    diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerTaintedTemplate.ql b/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerTaintedTemplate.ql new file mode 100644 index 000000000000..b2fe74c6d303 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerTaintedTemplate.ql @@ -0,0 +1,34 @@ +/** + * @id java/freemarker-tainted + * @name Tainted Freemarker Template + * @description Building a template from user-controlled sources is vulnerable to insertion of + * malicious code by the user. This may lead up to remote code execution and data leakage. + * @kind path-problem + * @problem.severity error + * @tags security + * external/cwe/cwe-094 + * @precision high + */ + +import java +import semmle.code.java.dataflow.FlowSources +import DataFlow::PathGraph +import semmle.code.java.dataflow.TaintTracking +import Freemarker + +class FreemarkerTaintedTemplateConfig extends TaintTracking::Configuration { + FreemarkerTaintedTemplateConfig() { this = "FreemarkerTaintedTemplateConfig" } + + override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { + any(Freemarker::NewTemplate t).getSink() = sink.asExpr() + or + any(Freemarker::FreemarkerPutTemplate t).getSink() = sink.asExpr() + } + // override int fieldFlowBranchLimit() { result = 0 } +} + +from DataFlow::PathNode source, DataFlow::PathNode sink, FreemarkerTaintedTemplateConfig conf +where conf.hasFlowPath(source, sink) +select sink.getNode(), source, sink, "Template is built using $@.", source.getNode(), "user input" diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerUnsafeConfiguration.ftl b/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerUnsafeConfiguration.ftl new file mode 100644 index 000000000000..8e640ceef93a --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerUnsafeConfiguration.ftl @@ -0,0 +1,17 @@ + + + + + +<#function msg text args...> + <#assign directive=title?interpret> + <#assign msg> + <@directive/> + + <#return msg> + + +

    ${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 input = new HashMap(); + input.put("title", argv[1]); + + Template template = cfg.getTemplate("FreemarkerUnsafeConfiguration.ftl"); + Writer consoleWriter = new OutputStreamWriter(System.out); + template.process(input, consoleWriter); + +} \ No newline at end of file diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerUnsafeConfiguration.qhelp b/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerUnsafeConfiguration.qhelp new file mode 100644 index 000000000000..65ea0c2826ff --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerUnsafeConfiguration.qhelp @@ -0,0 +1,36 @@ + + + + +

    +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. +

    + +
    + + +
  • +CVE-2021-25770:BeanShell Injection. +
  • +
    +
    diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerUnsafeConfiguration.ql b/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerUnsafeConfiguration.ql new file mode 100644 index 000000000000..d3b6fda945aa --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/FreemarkerUnsafeConfiguration.ql @@ -0,0 +1,21 @@ +/** + * @id java/freemarker-unsafe-configuration + * @name Unsafe Freemarker Configuration + * @description There is an unsafe Freemarker Configuration, that may lead to SSTI vulnerability + * that results in RCE. To protect against this + * 1) set class resolver to ALLOWS_NOTHING_RESOLVER, + * 2) dont set setAPIBuiltinEnabled to true + * 3) dont interpret user-input inside of template. + * @kind problem + * @problem.severity warning + * @tags security + * external/cwe/cwe-094 + * @precision high + */ + +import java +import Freemarker + +from Freemarker::FreemarkerTemplateConfigurationSource c +where not c.isSafe() +select c, "Unsafe Freemarker Configuration"