Safeguard
AppSec

org.apache.tomcat.embed: Embedded Tomcat Versions and Vulnerabilities

org.apache.tomcat.embed ships inside almost every Spring Boot jar, and its CVEs follow it there. Here is how to find your real embedded Tomcat version and patch it.

Marcus Chen
DevSecOps Engineer
8 min read

The org.apache.tomcat.embed group ID is the embeddable form of Apache Tomcat — the servlet container that Spring Boot, Grails, and many standalone Java services compile directly into their application jar — and it inherits nearly every Tomcat CVE without ever appearing in your own pom.xml. When a Tomcat advisory lands, most teams check their standalone Tomcat installs and stop there. The riskier copies are usually the ones buried three levels deep in a fat jar, pulled in transitively by spring-boot-starter-web, running a version nobody chose on purpose.

This post covers what the org.apache.tomcat.embed artifacts actually are, which recent vulnerabilities apply to embedded deployments, how to determine the version you are really running, and how to upgrade it safely when the fix arrives faster than your framework's release train.

What org.apache.tomcat.embed actually contains

Standalone Tomcat is a distribution: a bin/ directory, server.xml, a deployment folder. Embedded Tomcat is the same engine repackaged as plain Maven artifacts so a Java program can start a container in-process. The core pieces are:

  • org.apache.tomcat.embed:tomcat-embed-core — the HTTP connector, servlet engine, and request-processing pipeline. This is where most CVEs live.
  • org.apache.tomcat.embed:tomcat-embed-el — the Expression Language implementation.
  • org.apache.tomcat.embed:tomcat-embed-websocket — WebSocket support (JSR-356).
  • org.apache.tomcat.embed:tomcat-embed-jasper — the JSP compiler, only present if you render JSPs.

Spring Boot's spring-boot-starter-web pulls in tomcat-embed-core, tomcat-embed-el, and tomcat-embed-websocket by default. That means a "we don't use Tomcat" answer during an incident triage is almost always wrong for a Spring Boot shop — you use it on every request, you just never installed it.

Versioning tracks the main Tomcat line exactly: tomcat-embed-core 10.1.34 is the same code as standalone Tomcat 10.1.34 minus the packaging. When Apache publishes an advisory for Tomcat 9.0.x, 10.1.x, or 11.0.x, the embedded artifacts of the same version are affected identically unless the flaw sits in a component the embedded build omits (AJP is the classic example — embedded deployments rarely enable an AJP connector, which took CVE-2020-1938 "Ghostcat" off the table for most Spring Boot services even though it was a 9.8-severity file-read flaw fixed in 9.0.31, 8.5.51, and 7.0.100).

Recent CVEs that reached embedded Tomcat

Two advisories from the last two release cycles show how embedded exposure differs from standalone exposure.

CVE-2025-24813 (disclosed March 10, 2025) is a path-equivalence flaw in the partial PUT handling of the default servlet. Under a specific set of conditions — write-enabled default servlet, partial PUT enabled, and file-based session persistence or an attacker-useful upload path — it allows information disclosure or remote code execution. Affected ranges are Tomcat 9.0.0.M1 through 9.0.98, 10.1.0-M1 through 10.1.34, and 11.0.0-M1 through 11.0.2; fixes landed in 9.0.99, 10.1.35, and 11.0.3. Public exploitation attempts were observed within days of disclosure. For embedded users the mitigating detail is that the default servlet ships read-only, and Spring Boot does not enable write mode — but any team that toggled readonly=false for a quick file-upload feature inherited the full risk.

CVE-2024-50379 is a TOCTOU race in the same neighborhood: on case-insensitive filesystems (Windows, default macOS) with a write-enabled default servlet, concurrent read and upload of the same path can bypass case-sensitivity checks and get an uploaded file compiled as a JSP, yielding remote code execution. It affected 9.0.0.M1 through 9.0.97, 10.1.0-M1 through 10.1.33, and 11.0.0-M1 through 11.0.1, fixed in 9.0.98, 10.1.34, and 11.0.2.

The pattern to internalize: both flaws depend on configuration, and embedded configuration is set in Java code or framework properties rather than server.xml. Auditing it means auditing code, not a config file — which is exactly the kind of check that never happens unless a scanner forces the question.

Finding the version you are actually running

Your build file rarely states an embedded Tomcat version, because the framework's BOM chooses it. Three reliable ways to see the truth:

# Maven: show the resolved transitive version
mvn dependency:tree -Dincludes=org.apache.tomcat.embed

# Gradle
gradle dependencies --configuration runtimeClasspath | grep tomcat-embed

# Running process: ask the jar itself
unzip -p app.jar 'BOOT-INF/lib/tomcat-embed-core-*.jar' \
  META-INF/maven/org.apache.tomcat.embed/tomcat-embed-core/pom.properties

At runtime, ServerInfo.getServerNumber() (in org.apache.catalina.util) returns the exact build, and Spring Boot logs it at startup as Tomcat initialized with port(s). For fleet-wide answers, an SBOM generated at build time beats ad hoc queries: every fat jar's tomcat-embed-core entry is right there in the CycloneDX output, and an SCA tool such as Safeguard can flag the affected version range transitively the day the advisory publishes, instead of after someone remembers to grep.

Upgrading embedded Tomcat ahead of the framework

Spring Boot maps each patch release to a specific Tomcat version, but Apache does not schedule its security releases around Spring's calendar. When a Tomcat fix exists and the corresponding Boot release does not yet, override the managed version rather than waiting:

<!-- Maven: works because spring-boot-dependencies reads this property -->
<properties>
  <tomcat.version>10.1.35</tomcat.version>
</properties>
// Gradle with the Spring dependency-management plugin
ext['tomcat.version'] = '10.1.35'

Rules that keep this safe:

  1. Stay inside the same minor line (10.1.x to a later 10.1.x). Crossing from 9.0.x to 10.1.x changes the servlet API namespace from javax.servlet to jakarta.servlet and is a migration, not a patch.
  2. Remove the override once your framework's next patch release catches up, or you will silently pin an old version forever — the exact failure mode you were trying to escape.
  3. Re-run integration tests that exercise multipart uploads, WebSockets, and graceful shutdown; these are the areas where embedded Tomcat point releases most often change behavior.

Reducing the embedded attack surface

Beyond patching, embedded Tomcat gives you levers standalone admins would envy, because everything is programmatic:

  • Drop Jasper if you serve no JSPs. Excluding tomcat-embed-jasper removes the JSP compiler entirely, which neutralizes the whole class of "get a JSP written to disk, then execute it" attacks that both CVEs above rely on for their RCE path.
  • Keep the default servlet read-only. If you need file uploads, handle them in application code with an allow-listed destination outside the web root — never by flipping the servlet to writable.
  • Bind to localhost behind a reverse proxy and terminate TLS at the edge, so the embedded connector never faces raw internet traffic.
  • Cap request sizes (server.tomcat.max-swallow-size, max-http-form-post-size in Boot) to blunt resource-exhaustion probes.

None of this replaces upgrades. It shrinks the window in which an unpatched version is exploitable, which matters because the median gap between a Tomcat advisory and fleet-wide remediation in a large org is measured in weeks.

Making embedded Tomcat visible in your pipeline

The recurring failure with org.apache.tomcat.embed is not technical difficulty — the fix is a one-line version bump — it is visibility. The artifact is invisible in build files, absent from server inventories, and owned by nobody. Fixing the process looks like:

  • Generate an SBOM on every build and diff it, so a Tomcat version change (or lack of one) is reviewable.
  • Alert on the version range, not just the CVE ID, since NVD enrichment can lag the Apache announcement by days.
  • Track embedded Tomcat like a first-party dependency with a named owner, the same way you would treat your database driver.
  • Gate releases on scanner results so a jar carrying a known-exploited tomcat-embed-core cannot ship. Where that gate lives and how noisy it is allowed to be is a policy question — our post on false positives vs false negatives covers how to tune it without teams routing around it.

Embedded Tomcat also tends to travel with other high-churn parser dependencies — SnakeYAML in particular arrives through the same Spring Boot starters, with its own deserialization history covered in our SnakeYAML guide.

FAQ

Is org.apache.tomcat.embed the same as Apache Tomcat?

Yes — same codebase, same version numbers, same CVEs. The org.apache.tomcat.embed artifacts are Tomcat packaged as libraries so frameworks like Spring Boot can run the container in-process instead of deploying WAR files to a standalone install.

Which Spring Boot versions fixed CVE-2025-24813?

Any Spring Boot release that manages Tomcat 9.0.99, 10.1.35, or 11.0.3 and later carries the fix. If your Boot version pins an older Tomcat, override with the tomcat.version property rather than waiting for the next framework release.

Do I need tomcat-embed-jasper?

Only if you render JSP pages, which almost no modern Spring Boot service does. Excluding it removes the JSP compiler and with it the most common RCE endgame for Tomcat file-write vulnerabilities.

How do I check my embedded Tomcat version in production?

Check the startup log line (Tomcat initialized), inspect BOOT-INF/lib/ inside the deployed jar, or query your SBOM inventory. Relying on the version declared in a build file is unreliable because the framework BOM, not your pom, decides the resolved version.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.