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:
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):
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
healthyand that unauthenticatedGETto the protected resource returns401(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:
Download the full exploit bundle: exploit.zip.
2. Observe the output.
With a vulnerable server you will see output of this shape:
[CONTROL] GETon:8080→HTTP/1.1 401(resource is genuinely auth-gated).[EXPLOIT] POSTon:8080→HTTP/1.1 200with the full protected page body, including a lineSECRET: CVE-2026-43515-FLAG-<uuid>.[EXPLOIT] Disclosed secretsection prints thatSECRET:line, reflecting whatever the bypass returned, without any hard-coded value.[NEGATIVE] POSTon patched:8081→HTTP/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:
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
:8080→401(no secret in body) - Bypass POST on
:8080→200, body contained:SECRET: CVE-2026-43515-FLAG-4356b3e6-d0f3-4c45-9c6d-730333e5dc8a - Same POST on patched
:8081→401 - 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:
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:
- 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. - 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¶
- GHSA-5m62-pw8w-7w9f (GitHub Advisory) — affected/fixed versions, CVSS, CWE, patch commit links
- NVD CVE-2026-43515 — CVSS 9.1 CRITICAL, CWE-285
- Apache Tomcat Security Advisory (11.x) — affected 11.0.0-M1 – 11.0.21, fixed 11.0.22
- Apache Tomcat Security Advisory (10.x) — affected 10.1.0-M1 – 10.1.54, fixed 10.1.55
- Apache Tomcat Security Advisory (9.x) — affected 9.0.0.M1 – 9.0.117, fixed 9.0.118
- Patch commit for 11.x (276087d) —
RealmBase.javafix + regression test - Patch commit for 10.x (c621317) —
RealmBase.javafix + regression test - Patch commit for 9.x (db919ff) —
RealmBase.javafix + regression test - Apache OSS-Security announcement — public disclosure thread
- Apache mailing list thread — vendor advisory thread