Executive Summary
CVE-2026-40478 is a critical Server-Side Template Injection (SSTI) vulnerability in Thymeleaf — the most widely-used Java template engine in the Spring Boot ecosystem — affecting all versions through 3.1.3.RELEASE. A whitespace-parsing gap combined with an incomplete expression blocklist allows a remote, unauthenticated attacker to bypass Thymeleaf's built-in sandboxing and execute arbitrary Spring Expression Language (SpEL) code on the server, ultimately achieving full Remote Code Execution (RCE). Applications that render user-controlled data through Thymeleaf templates are at immediate risk, and upgrading to 3.1.4.RELEASE is the only complete fix.
1. What Is This Vulnerability?
Thymeleaf is a Java-based server-side template engine used in millions of Spring Boot applications to render dynamic HTML, email, and text output. It ships with a built-in sandbox designed to prevent untrusted input from being evaluated as executable expressions. CVE-2026-40478 is a bypass of that sandbox.
The Root Cause: Two Blind Spots in One
Security researchers at Endor Labs found that Thymeleaf's sanitization layer had two independent weaknesses that, when combined, fully defeated its protections:
1. Whitespace-parsing gap — The sanitizer normalized new ClassName() expressions but not new\tClassName() (with a tab character instead of a space). The real SpEL parser, however, treats tab characters as equivalent to spaces. The sanitizer said "safe"; SpEL said "execute."
2. Incomplete package blocklist — Thymeleaf's blocklist only covered java.* packages. Spring's own classes — including those that can spawn child processes — were left unrestricted. An attacker using Spring's ProcessBuilder wrapper or java.lang.Runtime through a T() operator on an aliased class could sidestep the list entirely.
Together, these allowed injection through the $${...} and nested #{...} pre-processing step, which is evaluated before the main template renderer applies security rules.
Technical Breakdown
When Thymeleaf processes a template string such as:
<!-- Vulnerable controller passes raw user input into model -->
<p th:text="${userInput}">Placeholder</p>
...and a Spring controller places unvalidated request data into userInput, an attacker can supply a payload like:
${T(java.lang.Runtime).getRuntime().exec('id')}
Or, to bypass the java.* blocklist using the tab-character trick:
${new java.lang.ProcessBuilder(new String[]{'id'}).start()}
The preprocessing step evaluates the SpEL expression before rendering, executing the system command on the server and returning the output to the attacker.
Attack Vector
The attack requires that:
- A Spring controller passes user-controlled data (request parameters, headers, path variables, form fields) directly into a Thymeleaf model attribute or template string without sanitization.
- The Alpha HTTP port (or application HTTP port) is reachable by the attacker — which in any Internet-facing application it is.
No authentication is required. No special privileges are needed. The attacker only needs to be able to send an HTTP request.
Real-World Impact
While no public breaches have been attributed to CVE-2026-40478 as of publication, SSTI vulnerabilities in Thymeleaf have historically been among the highest-exploited Java web flaws. The 2022–2024 Thymeleaf SSTI wave (CVE-2023-38286 and derivatives) was actively weaponized by multiple APT groups targeting Spring Boot microservices in financial and healthcare sectors. Given that CVE-2026-40478 is a bypass of the very defenses added after those incidents, exploitation is expected to follow quickly.
2. Who Is Affected?
| Component | Affected Versions |
|---|---|
thymeleaf (core) |
≤ 3.1.3.RELEASE |
thymeleaf-spring5 |
≤ 3.1.3.RELEASE |
thymeleaf-spring6 |
≤ 3.1.3.RELEASE |
thymeleaf-extras-* |
All versions paired with core ≤ 3.1.3 |
You are affected if all of the following are true:
- Your application uses Thymeleaf ≤ 3.1.3.RELEASE
- A controller method places user-controlled data (request parameters, path variables, form fields, headers, cookie values) into the Spring
ModelorModelAndView - That data is rendered via
th:text,th:utext,th:value,th:href, or similar Thymeleaf attribute expressions in a template
You are likely not affected if:
- All user data is HTML-escaped before it reaches the model (though defense in depth still recommends upgrading)
- You use a different template engine (Freemarker, Mustache, Pebble)
- You are on Thymeleaf 3.1.4.RELEASE or later
3. How to Detect It (Testing)
Manual Testing Steps
Step 1 — Identify injection points. Map all request parameters, path variables, form fields, and headers that are reflected in the application's HTML output. These are the candidate injection points.
Step 2 — Send a benign probe. Supply a simple SpEL probe that returns a predictable value with no side effects:
name=__$%7BT%28java.lang.String%29.valueOf%287*7%29%7D__
URL-decoded: name=__${T(java.lang.String).valueOf(7*7)}__
If the response contains __49__ instead of the literal string, the expression was evaluated — the application is vulnerable.
Step 3 — Test the tab-character bypass. If step 2 is blocked, try the whitespace bypass:
name=${new%09java.lang.ProcessBuilder(new+String[]{'id'}).start()}
%09 is a URL-encoded tab. A delayed response or error from the application indicates successful evaluation.
Step 4 — Check server side effects. For a safe, read-only confirmation, attempt:
${T(java.lang.System).getProperty('os.name')}
If the OS name appears in the response, RCE is confirmed.
Automated Scanning
Tool: OWASP ZAP with SSTI add-on
# Install SSTI scanner via ZAP marketplace, then run:
zap-cli quick-scan \
--spider \
--ajax-spider \
--scan \
--scanners ssti \
http://your-target-app.example.com
Expected output: ZAP flags any parameter where SSTI probes (${7*7}, #{7*7}, *{7*7}) return evaluated results.
Tool: tplmap (open source SSTI scanner)
python3 tplmap.py \
-u "http://your-app.example.com/greet?name=INJECT" \
--engine Thymeleaf \
--level 5
Expected output: [+] Thymeleaf plugin is pointing to a code injection
Tool: Snyk / Dependabot (dependency scanning)
# Maven
mvn snyk:test
# Gradle
./gradlew snykTest
Expected output: ✗ Critical severity vulnerability found in org.thymeleaf:thymeleaf@3.1.3.RELEASE
Code Review Checklist
- Search for
model.addAttribute(calls that includerequest.getParameter(,@RequestParam,@PathVariable, or header values - Confirm no raw user input flows into
TemplateEngine.process()as the template string itself - Verify
th:utext(unescaped text) is not used with user-controlled data anywhere in templates - Check that
thymeleaf.versioninpom.xmlorbuild.gradleis3.1.4.RELEASEor higher - Confirm Spring Boot's managed version of Thymeleaf is not being overridden by an older pinned version
4. How to Fix It (Mitigation)
Step-by-Step Remediation
1. Upgrade Thymeleaf to 3.1.4.RELEASE (the only complete fix).
For Maven (pom.xml):
<!-- If using Spring Boot's dependency management, override the managed version: -->
<properties>
<thymeleaf.version>3.1.4.RELEASE</thymeleaf.version>
</properties>
<!-- Or pin the dependency directly: -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring6</artifactId>
<version>3.1.4.RELEASE</version>
</dependency>
For Gradle (build.gradle):
ext['thymeleaf.version'] = '3.1.4.RELEASE'
// Or with version catalog (libs.versions.toml):
// thymeleaf = "3.1.4.RELEASE"
2. Verify the dependency resolved correctly.
# Maven
mvn dependency:tree | grep thymeleaf
# Gradle
./gradlew dependencies --configuration runtimeClasspath | grep thymeleaf
Expected: All Thymeleaf artifacts show 3.1.4.RELEASE.
3. Never pass raw user input as a template string. If your code calls templateEngine.process(userInput, context) where userInput comes from a request, rewrite it to pass only a safe template name/path, not the template content itself.
4. Deploy the updated artifact through your normal CI/CD pipeline and verify in staging before production rollout.
Code Fix Example
BEFORE (Vulnerable pattern):
@GetMapping("/greet")
public String greet(@RequestParam String name, Model model) {
// Directly placing user input into the model — dangerous if template renders it unsanitized
model.addAttribute("username", name);
return "greet"; // renders greet.html
}
<!-- greet.html — VULNERABLE: th:utext renders without escaping -->
<p th:utext="${username}">Guest</p>
AFTER (Safe pattern):
@GetMapping("/greet")
public String greet(@RequestParam String name, Model model) {
// Input validation: strip anything that isn't alphanumeric + spaces
String safeName = name.replaceAll("[^a-zA-Z0-9 ]", "");
model.addAttribute("username", safeName);
return "greet";
}
<!-- greet.html — SAFE: th:text HTML-escapes output by default -->
<p th:text="${username}">Guest</p>
The critical changes are: (1) upgrade Thymeleaf to 3.1.4.RELEASE to patch the sandbox bypass, (2) switch from th:utext to th:text so output is always HTML-escaped, and (3) validate and sanitize input before it enters the model.
Configuration Hardening
Even after upgrading, apply these additional controls:
- Disable the Thymeleaf cache in development only. In production,
spring.thymeleaf.cache=true(the default) prevents template re-compilation that could be abused. - Use a Content Security Policy (CSP) header to limit what injected scripts can do even if SSTI is somehow achieved:
Content-Security-Policy: default-src 'self'; script-src 'self' - Run the application with least privilege. The OS user running the JVM should not have write access outside its working directory, limiting what an RCE payload can do.
5. How to Test the Fix (Validation)
Regression Test Scenarios
- Scenario A: Submit the original probe
${7*7}as a name parameter. The rendered page must show the literal string${7*7}— not49. - Scenario B: Submit the tab-character bypass
${new\tjava.lang.ProcessBuilder(...).start()}. The application must reject or escape it without executing any command. - Scenario C: Submit a normal name like
Alice. The page must renderAlicecorrectly — confirming no functionality was broken by the fix.
Security Test Cases
Test Case 1: Verify the SSTI probe is no longer evaluated
- Precondition: Deploy application with Thymeleaf 3.1.4.RELEASE
- Steps:
GET /greet?name=%24%7B7*7%7D(URL-encoded${7*7}) - Expected Result: Response body contains the literal text
${7*7}or an escaped equivalent — not49
Test Case 2: Verify tab-character bypass is blocked
- Precondition: Same as above
- Steps:
GET /greet?name=%24%7Bnew%09java.lang.Runtime%7D - Expected Result: Literal string rendered or HTTP 400 — no command execution
Test Case 3: Verify normal rendering still works
- Precondition: Same as above
- Steps:
GET /greet?name=Alice - Expected Result: Response contains
Alice— application functions normally
Automated Tests
Add this JUnit/MockMvc test to your Spring Boot test suite:
@SpringBootTest
@AutoConfigureMockMvc
class SstiSecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
void ssti_probe_is_not_evaluated() throws Exception {
mockMvc.perform(get("/greet").param("name", "${7*7}"))
.andExpect(status().isOk())
.andExpect(content().string(not(containsString("49"))))
.andExpect(content().string(containsString("${7*7}")
.or(containsString("${7*7}"))));
}
@Test
void tab_character_bypass_is_blocked() throws Exception {
mockMvc.perform(get("/greet").param("name", "${new\tjava.lang.Runtime}"))
.andExpect(status().isOk())
.andExpect(content().string(not(containsString("java.lang.Runtime@"))));
}
@Test
void normal_name_renders_correctly() throws Exception {
mockMvc.perform(get("/greet").param("name", "Alice"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("Alice")));
}
}
6. Prevention & Hardening
Best Practices
- Never use user input as template content. Template names/paths must come from a controlled whitelist — never from request parameters. If you must render dynamic content, use
th:text(notth:utext) and validate inputs strictly. - Pin and audit dependency versions. Use a Software Bill of Materials (SBOM) tool such as
cyclonedx-maven-pluginto maintain an inventory of all transitive dependencies. Subscribe to GitHub Dependabot alerts and the Thymeleaf security mailing list. - Adopt a WAF rule for SSTI payloads. Deploy a Web Application Firewall with rules that block common SSTI syntax:
${},#{},*{},[[...]]in user-controlled query parameters and form fields. This is not a substitute for patching, but buys time and detects active exploitation attempts. - Enforce input validation at the controller layer. Use Bean Validation (
@Pattern,@Size) and a global@ControllerAdviceto reject malformed input before it touches the template layer. - Conduct regular DAST scanning with tools like OWASP ZAP, Burp Suite Pro, or Invicti as part of your CI/CD pipeline to catch SSTI and other injection-class vulnerabilities before they reach production.
Monitoring & Detection
Add the following to your application and infrastructure monitoring:
Log anomaly detection: Alert on any request parameter containing the character sequences ${, #{, *{, [[, or ]] appearing in access logs — these are Thymeleaf expression delimiters and have no legitimate place in user input.
Example logback pattern to flag suspicious requests:
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator>
<expression>
message.contains("${") || message.contains("#{") || message.contains("[[")
</expression>
</evaluator>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
Process-level monitoring: Alert on any unexpected child process spawned by your JVM process (e.g., sh, bash, cmd.exe, powershell). Tools like Falco (Linux) or Sysmon (Windows) can detect this behavior:
# Falco rule: detect shell spawned by Java process
- rule: Java Spawns Shell
desc: A Java process spawned an interactive shell — possible RCE exploitation
condition: >
spawned_process and
proc.pname in (java, java11, java17, java21) and
proc.name in (bash, sh, zsh, dash, cmd, powershell)
output: "Java spawned shell (parent=%proc.pname pid=%proc.ppid child=%proc.name)"
priority: CRITICAL
Runtime Application Self-Protection (RASP): Consider deploying a Java RASP agent (e.g., Contrast Security, Sqreen) that can block SpEL evaluation of dangerous patterns at runtime, regardless of library version.
References
- CVE Entry: NVD — CVE-2026-40478
- GitHub Security Advisory: GHSA — Thymeleaf SSTI Bypass
- Technical Writeup (Endor Labs): It's About Thyme: How a Whitespace Character Broke Thymeleaf's Expression Sandbox
- Patch / Release Notes: Thymeleaf 3.1.4.RELEASE on Maven Central
- CSO Online Coverage: Critical sandbox bypass fixed in popular Thymeleaf Java template engine
- DailyCVE Analysis: Thymeleaf Security Bypass leads to SSTI, CVE-2026-40478
- OWASP SSTI Reference: Server-Side Template Injection — OWASP