Skip to content

CVE-2026-43515 — Apache Tomcat Authorization Bypass via Multi-Collection Security Constraint

Published 2026-06-06

CVSS 9.1 CRITICAL

An unauthenticated attacker can retrieve protected resources from an Apache Tomcat server without providing any credentials, by sending a request using an HTTP method that falls through a flaw in constraint evaluation.

Project Apache Tomcat
Affected component RealmBase.findSecurityConstraints() in java/org/apache/catalina/realm/RealmBase.java
Severity CRITICAL
CVSS v3.1 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N (9.1)
CWE CWE-285
Affected versions 11.0.0-M1 – 11.0.21, 10.1.0-M1 – 10.1.54, 9.0.0.M1 – 9.0.117, 8.5.0 – 8.5.100 (EOL, no fix), 7.0.0 – 7.0.109 (EOL, no fix)
Fixed version 11.0.22, 10.1.55, 9.0.118
Advisory GHSA-5m62-pw8w-7w9f

1. Vulnerability overview

Apache Tomcat is a Java servlet container maintained by the Apache Software Foundation. This vulnerability allows an unauthenticated network attacker to read protected resources (those guarded by a <security-constraint> in web.xml) by sending a request using an HTTP method that the constraint's evaluation logic silently ignores.

The bug is present in all Tomcat releases from 9.0.0.M1 through 9.0.117, 10.1.0-M1 through 10.1.54, and 11.0.0-M1 through 11.0.21. It was publicly disclosed on 12 May 2026 and fixed in 9.0.118, 10.1.55, and 11.0.22.

Root cause

The flaw lives in RealmBase.findSecurityConstraints() in java/org/apache/catalina/realm/RealmBase.java. When a <security-constraint> in web.xml contains more than one <web-resource-collection> whose URL patterns match the request URI, Tomcat is supposed to check each collection's declared HTTP methods in turn and return every constraint that applies. The pre-fix code instead declared a boolean matched flag and a int pos index outside the per-collection loop. Once the first matching collection set matched = true and recorded its index in pos, the loop could no longer update pos because the flag was already true. After the loop, the method consulted only collection[pos].findMethod(method). Every subsequent collection that also matched the URL pattern was never examined for its method list.

Vulnerable code (abbreviated):

// BEFORE (buggy) — boolean matched / int pos declared OUTSIDE the j-loop
boolean matched = false;
int pos = -1;
for (int j = 0; j < collection.length; j++) {
    String[] patterns = collection[j].findPatterns();
    // ... skips non-extension patterns ...
    for (int k = 0; k < patterns.length && !matched; k++) {
        // extension-pattern matching logic ...
        if (pattern.regionMatches(1, uri, dot, uri.length() - dot)) {
            matched = true;
            pos = j;   // records only the FIRST matching collection
        }
    }
}
// Evaluated only collection[pos]; all later matching collections ignored
if (matched) {
    found = true;
    if (collection[pos].findMethod(method)) {
        if (results == null) { results = new ArrayList<>(); }
        results.add(constraints[i]);
    }
}

Patch diff (identical across the 9.x, 10.x, and 11.x branch commits):

-            boolean matched = false;
-            int pos = -1;
             for (int j = 0; j < collection.length; j++) {
                 String[] patterns = collection[j].findPatterns();

                 // ... skips non-extension patterns ...

+                boolean matched = false;
                 for (int k = 0; k < patterns.length && !matched; k++) {
                     String pattern = patterns[k];
                     if (pattern.startsWith("*.")) {
                         // extension-pattern matching logic
                         if (pattern.regionMatches(1, uri, dot, uri.length() - dot)) {
                             matched = true;
-                            pos = j;
                         }
                     }
                 }
-            }
-            if (matched) {
-                found = true;
-                if (collection[pos].findMethod(method)) {
-                    if (results == null) { results = new ArrayList<>(); }
-                    results.add(constraints[i]);
+                if (matched) {
+                    found = true;
+                    if (collection[j].findMethod(method)) {
+                        if (results == null) { results = new ArrayList<>(); }
+                        results.add(constraints[i]);
+                    }
                 }
             }

The fix moves boolean matched inside the j (collection) loop so it resets to false for each collection, eliminates the stale pos variable entirely by using the loop index j directly, and moves the per-collection check inside the same loop so every matching collection is evaluated independently against the requested HTTP method.


2. Vulnerable environment

The reproduction uses two official Apache Tomcat Docker images running as sibling containers on a private bridge network: tomcat:10.1.54-jre17 (the last affected 10.1.x build) as the target, and tomcat:10.1.55-jre17 (the first fixed 10.1.x build) as a patched comparison server. The Tomcat binaries come unaltered from the official images; only a protected webapp, a realm user file, and a fresh-secret entrypoint are layered on top.

Service Role Image Port
vulnerable Exploit target — Tomcat 10.1.54 tomcat:10.1.54-jre17 127.0.0.1:8080
patched Patched comparison — Tomcat 10.1.55 tomcat:10.1.55-jre17 127.0.0.1:8081

Download the environment bundle (env.zip) or browse individual files: env/Dockerfile, env/docker-compose.yml.

Bringing the environment up:

docker compose -f env/docker-compose.yml up -d --build

Confirming the vulnerable version is running:

# Expect: Server number: 10.1.54.0
docker exec cve-2026-43515-tomcat-vulnerable sh -c 'catalina.sh version 2>/dev/null | grep "Server number"'

# Expect: Server number: 10.1.55.0
docker exec cve-2026-43515-tomcat-patched   sh -c 'catalina.sh version 2>/dev/null | grep "Server number"'

Confirming the exploit surface is real (authenticated GET is withheld):

# Expect: 401
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/protected/secret.html

The web.xml deployed in the vulnerable webapp contains the following security constraint, which is the configuration that triggers the bug:

<security-constraint>
  <web-resource-collection>
    <web-resource-name>Protected GET</web-resource-name>
    <url-pattern>*.html</url-pattern>
    <http-method>GET</http-method>
  </web-resource-collection>
  <web-resource-collection>
    <web-resource-name>Protected POST</web-resource-name>
    <url-pattern>*.html</url-pattern>
    <http-method>POST</http-method>
  </web-resource-collection>
  <auth-constraint>
    <role-name>admin</role-name>
  </auth-constraint>
</security-constraint>

Both collections share the *.html URL extension pattern. GET is listed in the first collection, POST in the second. An <auth-constraint> requiring the admin role applies to the entire constraint. With the buggy code, Tomcat only consults the first collection when evaluating POST, finds that it lists GET (not POST), and therefore treats POST as unconstrained, letting an unauthenticated request through.

The entrypoint script (env/config/entrypoint.sh) regenerates /protected/secret.html on every container boot with a fresh CVE-2026-43515-FLAG-<uuid> secret. The secret is never baked at build time, so its value is unknown in advance; a match between the exploit output and the independently read value proves the resource was genuinely retrieved rather than guessed.


3. How to exploit

Prerequisites

  • Bring up the environment as described in §2.
  • Confirm both containers report healthy and that unauthenticated GET to the protected resource returns 401 (confirms the auth gate is real).

Steps

1. Run the exploit script.

The script (exploit/run.sh) issues three requests: a control GET to confirm the resource is auth-gated, the bypass POST, and the same POST against the patched server to confirm it withholds the resource:

bash exploit/run.sh http://127.0.0.1:8080 /protected/secret.html http://127.0.0.1:8081

Download the full exploit bundle: exploit.zip.

2. Observe the output.

With a vulnerable server you will see output of this shape:

  • [CONTROL] GET on :8080HTTP/1.1 401 (resource is genuinely auth-gated).
  • [EXPLOIT] POST on :8080HTTP/1.1 200 with the full protected page body, including a line SECRET: CVE-2026-43515-FLAG-<uuid>.
  • [EXPLOIT] Disclosed secret section prints that SECRET: line, reflecting whatever the bypass returned, without any hard-coded value.
  • [NEGATIVE] POST on patched :8081HTTP/1.1 401 (secret withheld).

3. Verify the disclosed secret against the independent privileged channel.

Because the exploit prints whatever the server returns, the only trustworthy proof that it read a genuinely protected value (rather than a public page or a fixed string) is to compare the disclosed value against the true current secret, read through a channel that is completely independent of the exploit's HTTP path:

docker exec cve-2026-43515-tomcat-vulnerable cat /seed/secret_value

The entrypoint writes the boot's secret to the root-owned, 0600 /seed/secret_value file before starting Tomcat. The exploit has no access to this file and can only read what the HTTP response returns. Matching values confirm the bypass surfaced the protected content.

Observed evidence from the verified run

The reproduction was confirmed by the following evidence:

  • Vulnerable server version: Server number: 10.1.54.0
  • Patched server version: Server number: 10.1.55.0
  • Control GET on :8080401 (no secret in body)
  • Bypass POST on :8080200, body contained: SECRET: CVE-2026-43515-FLAG-4356b3e6-d0f3-4c45-9c6d-730333e5dc8a
  • Same POST on patched :8081401
  • Privileged channel (true current secret): CVE-2026-43515-FLAG-4356b3e6-d0f3-4c45-9c6d-730333e5dc8a
  • The exploit-disclosed value matches the privileged-channel value exactly.

To confirm the secret is genuinely per-boot rather than a fixed build-time value, the run recorded two consecutive boots and verified the secret changed between them:

  • Boot 1 secret: CVE-2026-43515-FLAG-a2def6ba-7c53-4839-90e0-04c2e441a3b5
  • Boot 2 secret: CVE-2026-43515-FLAG-4356b3e6-d0f3-4c45-9c6d-730333e5dc8a

The same exploit run against the patched 10.1.55 server produced 401, and its independently seeded secret (CVE-2026-43515-FLAG-4d92cd84-f79d-4bd7-8fd5-cc686dcc4649) differed from the vulnerable container's value, confirming the patched negative is genuine rather than a coincidence.

The bypass is 100% deterministic: it fires every time the described web.xml configuration is present, with no race condition, no timing dependency, and no heap manipulation required.

Teardown

When you are done, stop and remove the containers and their volumes:

docker compose -f env/docker-compose.yml down -v

4. Security advice

Remediation

Upgrade to one of the fixed releases:

Branch Fixed version
11.x 11.0.22
10.1.x 10.1.55
9.x 9.0.118

Tomcat 8.5.x and 7.x are end-of-life and Apache has not issued patches for those branches. Operators still running them should prioritize upgrading.

Mitigations and workarounds

If an immediate upgrade is not possible, audit every <security-constraint> in every deployed web.xml. The vulnerable pattern is a single <security-constraint> that contains two or more <web-resource-collection> blocks sharing the same URL extension pattern (e.g., *.html, *.jsp) but listing different HTTP methods. Any application relying on this pattern for access control is unprotected on the methods listed in non-first collections.

Where restructuring web.xml is possible, two mitigations are practical:

  1. Merge all methods that require the same auth constraint into a single <web-resource-collection> block with the shared URL pattern and an <http-method> for each protected method.
  2. Alternatively, if the intent is to protect all methods, remove the <http-method> elements entirely; a collection with no method list covers all HTTP methods.

If the protected resource is behind additional layers of access control (a WAF, reverse proxy, or application-level auth checks), those layers may reduce practical exposure, but they do not fix the container-level bypass.

Note: Apache's own security page rates this vulnerability "Moderate" despite the NVD/GHSA CVSS 9.1 CRITICAL score. The actual impact depends on what sits behind the constraint; if the guarded resources are sensitive, the practical severity is high.

References