Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java: Introduce Freemarker for SSTI queries #6320

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions java/ql/src/experimental/Security/CWE/CWE-094/Freemarker.qll
Original file line number Diff line number Diff line change
@@ -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) }
}
}
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
<qhelp>

<overview>
<p>
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.
</p>
</overview>

<recommendation>
<p>
To fix this, ensure that an untrusted value is not used as a template.
</p>
</recommendation>

<example>
<p>
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.
</p>
<sample src="FreemarkerTaintedTemplate.java" />
</example>

<references>
<li>Portswigger : [Server Side Template Injection](https://portswigger.net/web-security/server-side-template-injection)</li>
</references>

</qhelp>
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<html>
<head>
</head>
<body>

<#function msg text args...>
<#assign directive=title?interpret>
<#assign msg>
<@directive/>
</#assign>
<#return msg>
</#function>

<p>${m.msg(title)}</p>

</body>
</html>
Original file line number Diff line number Diff line change
@@ -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<String, Object> input = new HashMap<String, Object>();
input.put("title", argv[1]);

Template template = cfg.getTemplate("FreemarkerUnsafeConfiguration.ftl");
Writer consoleWriter = new OutputStreamWriter(System.out);
template.process(input, consoleWriter);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>

<overview>
<p>
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 <code>interpret</code> filter function.
It results in executing user-controlled data as part of template, which may lead up to remote code execution.
</p>
</overview>

<recommendation>
<p>
It is generally recommended to avoid using <code>interpret</code> filter function.
Additionally you should configure template engine by setting class resolver to <code>ALLOWS_NOTHING_RESOLVER</code>.
</p>
<p>
Also there is method <code>setAPIBuiltinEnabled</code> which enables usage of builtin API.
By default this property is set to false. And setting this value to true is unsafe.
</p>
</recommendation>

<example>
<p>
The following example pass untrusted data through data model, then interpret it as user definec directive and execute it.
</p>
<sample src="FreemarkerUnsafeConfiguration.java" />
</example>

<references>
<li>
CVE-2021-25770:<a href="https://www.synacktiv.com/en/publications/exploiting-cve-2021-25770-a-server-side-template-injection-in-youtrack.html">BeanShell Injection</a>.
</li>
</references>
</qhelp>
Original file line number Diff line number Diff line change
@@ -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"