Thymeleaf is the default template engine bundled with Spring Boot, which means the sandbox protecting its expression language sits underneath an enormous share of production Java web apps. CVE-2026-40478, patched in Thymeleaf 3.1.4 and disclosed alongside fixes in Spring Boot 3.5.14 and 4.0.6, shows how fragile that sandbox can be: a security check meant to block the new keyword was defeated by swapping the space after it for a tab character. Researcher Dawid Bakaj found that Thymeleaf's pre-check only matched the exact string new immediately followed by a space character, while the underlying SpEL/OGNL-style expression parser is far more permissive about what counts as a separator — it happily accepts a tab there too — so new[TAB] slipped past the filter and still constructed the object. Combine that with a type blocklist that stopped at java.* packages and never touched Spring's own classes, and an attacker could reach org.springframework.core.io.FileSystemResource, write a JSP file to disk, and chain toward full remote code execution. Endor Labs published independent technical analysis confirming the mechanism. Critically, this only bites applications that feed untrusted input into the expression engine itself — not the vast majority of Thymeleaf apps using it as intended. This post breaks down why that distinction matters and how to stay on the safe side of it.
What made the CVE-2026-40478 bypass possible?
Two independent gaps had to align. First, Thymeleaf's expression sandbox used a lexical pre-check for object instantiation: it scanned for the exact string new followed by a space and rejected it. SpEL's real tokenizer, however, treats any whitespace character — including a tab — as a valid separator between the new keyword and a class name, so new[TAB]org.springframework.core.io.FileSystemResource(...) never triggered the check but still parsed and executed identically to the space-separated form. Second, the sandbox's type blocklist was scoped to java.*, leaving every class in org.springframework.* unrestricted. FileSystemResource was never designed as a dangerous primitive, but paired with getOutputStream() it becomes an arbitrary file-write, and a written .jsp file dropped into a servlet container's webroot executes on the next request. Neither gap alone was exploitable; the CVSS 9.1 score reflects what happens when a whitespace edge case and an incomplete blocklist combine on a code path that reaches expression evaluation.
Which Thymeleaf usage patterns are actually vulnerable?
The vulnerable pattern is passing user-controlled strings into Thymeleaf's expression or template-resolution engine rather than keeping templates static and passing data through the model. Concretely: building a th:text or inline [[${...}]] fragment by concatenating a request parameter into the template string before parsing, or letting a page or name query parameter select which view template to render without an allowlist. In both cases, attacker-supplied text ends up somewhere Thymeleaf treats as executable expression syntax rather than as inert data. Standard Thymeleaf usage — a controller populates a Model object, and the template references ${myModelAttribute} — is not affected, because the expression itself is static and only the value is attacker-influenced, not the code. This mirrors the well-known 2021 Thymeleaf natural-templates issue (CVE-2021-33036) and years of Spring SpelExpressionParser findings: the recurring root cause across all of them is untrusted input reaching a parser instead of a data binding.
How should teams remediate and verify the fix?
The direct fix is a version bump: Thymeleaf 3.1.4 or later, and Spring Boot 3.5.14+ or 4.0.6+, which ship whitespace normalization ahead of keyword checks and an expanded, allowlist-oriented type policy instead of a java.*-only blocklist. Because this is a parser-level fix, upgrading resolves the specific tab-bypass technique without requiring template rewrites. But teams should treat the version bump as necessary, not sufficient: any code that constructs template strings, view names, or expression fragments from request data is worth auditing on its own merits, since a future bypass technique against the same class of blocklist-based sandbox is not something anyone can rule out in advance. Grep for string concatenation feeding into TemplateEngine.process(), SpringTemplateEngine, or dynamic ViewResolver name construction, and confirm every one of those call sites either uses a fixed template with model-bound data or validates input against a strict allowlist before it ever reaches the engine.
Why do expression-language sandboxes keep failing this way?
Blocklist-based sandboxes for expression languages have a structural weakness: they enumerate what they've thought of, and an expression parser like SpEL or OGNL has a large enough grammar that lexical pre-checks rarely cover the same surface the real parser accepts. This is the same category of failure behind years of Java deserialization gadget-chain research and template-engine escapes in other ecosystems — a safety layer bolted in front of a Turing-complete-ish evaluator is checking syntax, while the evaluator itself understands semantics. The durable fix isn't a better blocklist; it's not handing attacker-controlled strings to the evaluator in the first place. Frameworks that support "restricted expression objects" (a fixed API surface the expression can call, with no path to new, reflection, or classloading) close the gap more reliably than pattern-matching input text, because there's no equivalent syntax to bypass — the object model itself doesn't expose dangerous operations.
How Safeguard helps
Safeguard's SAST engine traces dataflow across Java source, so a request parameter that flows into a TemplateEngine.process() call or a dynamically built view name shows up as a source-to-sink finding with the full trace, not a bare pattern match on "uses Thymeleaf." Reachability analysis then confirms whether that sink is actually invoked from a live entry point in your call graph before it's ranked urgent — and, importantly, code that reaches an expression engine through reflection or dynamic dispatch is marked unknown rather than silently suppressed, which matters directly for SSTI-style sinks where the dangerous call is often one layer removed from the obvious pattern. On the dependency side, Safeguard's SCA and reachability pipeline flags outdated Thymeleaf and Spring Boot versions against CVE-2026-40478 and correlates that with whether your build actually pulls in the affected release, so the fix — upgrading to 3.1.4+ and Spring Boot 3.5.14+/4.0.6+ — lands as a prioritized, actionable finding instead of one more line in a dependency report.