CVE-2026-40478 Thymeleaf Template Injection: From Sandbox Bypass to Unauthenticated RCE

image

Overview

  • Published: 04/17/2026
  • CVE-ID: CVE-2026-40478
  • CVSS: 9.1 Critical
  • Affected Engine: Thymeleaf Server Side Template Engine
  • Affected Versions: 3.1.3.RELEASE and prior
  • CWE: CWE-917: Improper Neutralization of Special Elements used in an Expression Language Statement (‘Expression Language Injection’) and CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine

Description

Thymeleaf is a server-side Java template engine for web and standalone environments. Versions 3.1.3.RELEASE and prior contain a security bypass vulnerability in the the expression execution mechanisms. Although the library provides mechanisms to prevent expression injection, it fails to properly neutralize specific syntax patterns that allow for the execution of unauthorized expressions. If an application developer passes unvalidated user input directly to the template engine, an unauthenticated remote attacker can bypass the library’s protections to achieve Server-Side Template Injection (SSTI). This issue has ben fixed in version 3.1.4.RELEASE.

Root Cause Analysis

During this blog I will analyze 3 main root causes :

Root Cause 1 : TOCTOU in detect expression step

Some view name/request parameter processing flows check if the input contains a dangerous expression. However, this check occurs before Thymeleaf preprocessing transforms the input.

The preprocessing type string can cause the initial check to miss ${...}, but after Thymeleaf processing, it becomes the actual expression that is evaluated. This is a case of Time-of-Check to Time-of-Use: the data representation at the time of checking and at the time of use are not the same.

We can understand in simple way that :

Input raw         -> check: no danger expression
Preprocessing     -> change into expression
Expression engine -> evaluate expression

Root Cause 2 : Mismatch whitespace between Thymeleaf normalizer and SpEL parser

This is the cause that make this CVE became popular and fun in some way.

As I read that thymeleaf have it own normalize/check to find danger pattern like new , T(, param, #. But with TAB key, thymeleaf normalize handle it different compare with SpEL parser.

In details that :

new<TAB>ClassName

When it go through ExpressionUtils.normalize(), TAB been strip so the string become like this :

newClassName

The scanner no longer sees the pattern new + whitespace. However, the SpEL parser receives the original input, and SpEL considers TAB as a valid whitespace, so it still parses it as:

new ClassName

Result: Thymeleaf thinks there’s no new object, but SpEL still creates the object.

This is a classic design flaw: the security filter uses a different grammar than the actual parser.

Root Cause 3 : BlockList type not enough

Even without directly calling java.lang.Runtime or ProcessBuilder, an attacker can still find other classes in the classpath. The article that i read points out two primitives commonly found in Spring Boot:

com.fasterxml.jackson.databind.ObjectMapper
org.springframework.core.io.FileSystemResource

FileSystemResource#getOutputStream() can open a file write stream, while JacksonObjectMapper#createGenerator() can write raw content to that stream. Because these classes are not in the original blocklist, an attacker can use them to write to arbitrary files within the Java process’s permissions.

NVD also has a related CVE, CVE-2026-40477, which describes Thymeleaf not properly limiting the scope of accessible objects, leading to attackers accessing sensitive objects from templates.

Complete Exploit Chain

mermaid-diagram

Exploit chain of CVE-2026-40478: attacker-controlled input reaches a Thymeleaf expression context, bypasses sandbox checks due to preprocessing and whitespace normalization inconsistencies, and is eventually evaluated by SpEL. This enables object construction and method invocation, leading to arbitrary file write and potentially RCE depending on the runtime environment.

Build And Analysis + POC

image

First of all is checking the version, at my local POC, i’ve using version 3.1.3.RELEASE that vulnerable.

After create project and make sure the pom.xml is going right way now is the java code auditing step.

Source to Sink explain

Safe Endpoint

In this lab, the vulnerability is introduced in the /unsafe endpoint because user-controlled input is directly inserted into a Thymeleaf expression context.

@GetMapping("/safe")
    public String safe(@RequestParam(name = "name", defaultValue = "guest") String name) {
        Context ctx = new Context();
        ctx.setVariable("name", name);

        String template = "<p th:text=\"${name}\">fallback</p>";
        return engine.process(template, ctx);
    }

This is the source.

However, the value is not inserted into the template as executable expression code. Instead, it is stored as a variable inside the Thymeleaf context:

ctx.setVariable("name", name);

The template expression is static:

th:text="${name}"

This means Thymeleaf only evaluates the fixed expression ${name} and then reads the value of the name variable from the context.

So even if the attacker sends:

/safe?name=${7*7}

The value ${7*7} is treated as plain data, not as a new expression to be evaluated.

Unsafe Endpoint

@GetMapping("/unsafe")
    public String unsafe(@RequestParam(name = "expr") String expr) {
        Context ctx = new Context();

        String template = "<p th:text=\"${" + expr + "}\">fallback</p>";
        return engine.process(template, ctx);
    }

In the /unsafe endpoint, the user-controlled value comes from the expr request parameter:

@RequestParam(name = "expr") String expr

This is the source.

The dangerous part is here:

String template = "<p th:text=\"${" + expr + "}\">fallback</p>";

The application concatenates user input directly into a Thymeleaf expression. This changes the role of the input. It is no longer treated as data. It becomes part of the expression code that Thymeleaf will parse and evaluate.

Then the final template is passed into the template engine:

return engine.process(template, ctx);

This is the sink.

For example, if the attacker sends:

/unsafe?expr=7*7

The server builds this template internally:

<p th:text="${7*7}">fallback</p>

When Thymeleaf processes the template, it evaluates ${7*7} and returns:

<p>49</p>

This confirms that attacker-controlled input has reached an expression evaluation sink.

POC Proof Of Concept

First i will test the safe endpoint with payload using {{7*7}} expression to test that does it evaluate the value.

image

As we can see that the content fallback not contain the value after calculate so that we can confirm that it not vulnerable because it not evaluate.

The unsafe endpoint concatenates user input directly into ${…}. Simple arithmetic works because there’s no blacklist bypass needed yet:

image

Output:

<p>49</p>

→ Confirms expression evaluation is possible.

Bypass the Blacklist with TAB (%09) Thymeleaf’s security check looks for dangerous patterns like new (space after new) and T(. If you use TAB (%09) instead of space, Thymeleaf’s normalizer removes it during check, but SpEL still parses it as whitespace.

Test object instantiation – bypass detection:

image

Output:

<p>hello</p>

→ New followed by TAB is not blocked, and SpEL creates the string successfully.

Mitigation

The most effective mitigation is to upgrade Thymeleaf to a patched version. CVE-2026-40478 affects Thymeleaf versions up to and including 3.1.3.RELEASE, and the advisory lists 3.1.4.RELEASE as the patched version for this issue. However, because a later Thymeleaf sandbox bypass issue, CVE-2026-41901, affects versions prior to 3.1.5.RELEASE, applications should upgrade to 3.1.5.RELEASE or later whenever possible.