
Offline checker for Thymeleaf CVE-2026-40477 / CVE-2026-41901 — tells you which of the two CVSS 9.0 SSTI flaws you are exposed to, and whether your version line has a fix at all (3.0.x: it does not)
Thymeleaf CVE-2026-40477 / CVE-2026-41901 offline triage tool — zero dependencies, single jar, no network.
Two CVSS 9.0 SSTI bypasses, with two fixed releases shipped within nine days. It first answers "which of these am I affected by," then answers the harder question:
On my version line, is there a fixed release I can upgrade to?
For 3.1.x, yes (just upgrade to 3.1.5.RELEASE and you're done).
For 3.0.x and earlier, no — and that is precisely the largest installed base.
The affected ranges in both advisories are introduced: 0 in OSV:
curl -s https://api.osv.dev/v1/vulns/GHSA-r4v4-5mwr-2fwr | jq '.affected[0].ranges'
# [{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.1.4.RELEASE"}]}]
In other words, the entire 3.0 line is in scope; the per-version list runs from 3.0.15.RELEASE all the way down to 1.0.0.
And on Maven Central the 3.0 line ends at 3.0.15.RELEASE, with no releases after that:
curl -s https://repo1.maven.org/maven2/org/thymeleaf/thymeleaf/maven-metadata.xml \
| grep -o '<version>3\.0\.[^<]*</version>' | tail -1
# <version>3.0.15.RELEASE</version>
The fixed releases for both CVEs are on the 3.1 line. So 3.0.x users don't have the option of "just bump the version number" — only a cross-major-version migration — and 3.1 is an officially listed breaking change (copied verbatim from the 3.1.0.M1 section of the official ChangeLog.txt):
- Removed web-API based expression security objects (#request, #response, #session, #servletContext).
- Removed support for Spring 3.x and Spring 4.x.
- Set minimum JDK compatibility level to JDK 8 project-wide (JDK 17 for thymeleaf-spring6).
If your templates use ${#request.…} / ${#session.…}, upgrading will fail outright; those values must be moved into the Model by the Controller.
This is a migration that needs to be scheduled, not a version bump.
Dependent counts from deps.dev (read directly on 2026-09-04, reproducible by anyone):
| Version | Dependents | CVEs hit | Same-line upgrade possible? |
|---|---|---|---|
| 3.0.11.RELEASE | 1124 | both | ❌ no fixed release on the 3.0 line |
| 3.0.15.RELEASE | 678 | both | ❌ same as above, and it is already the end of that line |
| 3.0.12.RELEASE | 641 | both | ❌ |
| 3.1.3.RELEASE | 826 | both | ✅ upgrade to 3.1.5 |
| 3.1.2.RELEASE | 677 | both | ✅ |
| 3.1.4.RELEASE | 70 | only CVE-2026-41901 | ✅ |
| 3.1.5.RELEASE | 590 | — | already safe |
The single most-installed version is 3.0.11.RELEASE, 36% higher than 3.1.3, the highest on the 3.1 line — and it has no same-line way out.
java -jar thymeleaf-check.jar <directory or jar/war path> [--utf8|--gbk]
$ java -jar thymeleaf-check.jar ./target
[CRITICAL] ./target/myapp.war :: BOOT-INF/lib/thymeleaf-3.0.11.RELEASE.jar
构件:thymeleaf 版本 3.0.11.RELEASE(3.0 线,依据: MANIFEST(Implementation-Version))
命中: CVE-2026-40477 + CVE-2026-41901
受影响,且 3.0 线在 Maven Central 上没有任何修复版(该线止于 3.0.15.RELEASE)。
Exit codes: 2 = an artifact's version line has no same-line fixed release · 1 = affected but a same-line upgrade exists / cannot be determined · 0 = nothing found · 3 = usage or path error.
Can be wired directly into CI.
Scan build artifacts (target/*.jar, *.war), not source directories.
Thymeleaf is almost always pulled in transitively by spring-boot-starter-thymeleaf, and the starter itself carries no version —
so the Thymeleaf version is simply not visible in the pom. In that case the tool explicitly says "cannot be determined from the pom" rather than pretending it isn't there.
Three identification sources, ordered by reliability:
Implementation-Version — still recognized even if a repackaging tool renamed the jar.
(Thymeleaf's official jar has only MANIFEST.MF under META-INF/, no maven/ directory, so for it this is the strongest source.)pom.propertiesBOOT-INF/lib/ and WEB-INF/lib/ inside fat jars / wars are opened and inspected individually, and the MANIFEST of embedded jars is read too.
CVE-2026-40477 | CVE-2026-41901 | |
|---|---|---|
| GHSA | GHSA-r4v4-5mwr-2fwr | GHSA-c9ph-gxww-7744 |
| CVSS | 9.0 | 9.0 |
| Affected | <= 3.1.3.RELEASE | <= 3.1.4.RELEASE |
| Fixed | 3.1.4.RELEASE | 3.1.5.RELEASE |
| Disclosed | 2026-04-15 | 2026-05-04 |
| Official wording | "fails to properly restrict the scope of accessible objects" | "fails to properly neutralize specific constructs", occurring in a sandboxed (restricted) context |
The affected range of the second includes 3.1.4, so anyone who upgraded to 3.1.4 based on the first advisory falls within the second.
But don't read this as "the maintainers didn't fix it properly" — the two advisories describe different mechanisms,
and the two official releases 3.1.4 (2026-04-12) and 3.1.5 (2026-04-21) are nine days apart,
with no official statement saying the earlier fix was incomplete. This tool only lays out the facts side by side; it does not draw that conclusion for you.
Every number in RuleTable.java can be traced back:
python tools/gen_rules.py --show
It cross-checks the rule table against two primary sources — OSV's affected ranges and per-version lists, and Maven Central's
maven-metadata.xml — and exits non-zero on any mismatch.
Three conventions:
mvn package # JDK 17;target/thymeleaf-check-0.1.0.jar
mvn test # 19 test cases
Zero runtime dependencies is deliberate: a triage tool must be droppable into any environment and just run.
Apache-2.0
| Code | Meaning |
|---|---|
0 | Scan completed, and every file was actually read, with no findings requiring action |
4 | Some file could not be read — not a zip, truncated content, or read failure |
🔴 4 was added on 2026-09-09, and the reason is worth spelling out.
Before that, unreadable files only left a line of notice in the output, while the exit code remained 0 —
leaving a trace is for humans, CI and scripts look at the exit code, so "I couldn't read it" equalled "passed" in automation.
A corrupt, encrypted, or incompletely downloaded jar would quietly turn into "no problems found."
"I couldn't read it" and "you are safe" must be two different statements.
⚠️ A legitimate empty jar does not count as unreadable (a 22-byte EOCD record is a real empty zip) and will not trigger 4 —
when there are too many false alarms, nobody looks at the real ones.
The meanings of the remaining exit codes are given in the decision tiers and usage notes above.