ATS 10.2.0 mixed-case HTTP/1 skips ip_allow deny
Apache Traffic Server 10.2.0 HTTP/1 mixed-case DELETE/TRACE skip default ip_allow.yaml deny and TUNNEL to the remapped origin as the ATS uid. Cache PURGE/PUSH stay closed. HTTP/2 :method nocase still denied. Labbed. No CVE yet.
- Name
- ATS 10.2.0 mixed-case HTTP/1 skips ip_allow deny
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- high
- Disclosure Status
- public
- Vendor
- Apache Traffic Server (ASF)
- Affected
- Apache Traffic Server 10.2.0 (b1df7334) default reverse proxy. remap_required 1 plus one map. HTTP/1 from a non-loopback client.
- Published
- 13 Oct 2026
- Updated
- 14 Oct 2026
- Tags
- 0-day, trafficserver, ip_allow, privilege-leftover, remote, cwe-178, cwe-863
DELETE is denied. delete is not.
The proof of concept is on GitHub: abraxas/ats-ipallow-method-case (loopback client; @abraxas_null). The lab stack is lab/: Dockerfile, docker-compose.yml, run.sh, poc.py, origin.py, configs/ip_allow.yaml. Authorized lab only. It talks to loopback.
This is Apache Traffic Server 10.2.0 (b1df7334b39a878971c376da849e8da4984918b6). No CVE yet. CWE-178 / CWE-863. 7.5 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N). Remote privilege leftover. Default reverse proxy. HTTP/1 from a non-loopback client.
Stock ip_allow.yaml denies PURGE, PUSH, DELETE, and TRACE from 0/0 and ::/0. YAML parse is nocase. HTTP/2 :method is nocase. HTTP/1 tokenize then strncmps the request bytes against the well-known string and returns -1 on case mismatch. Deny invert treats that unknown token as allowed. ATS classifies it as METHOD_NOT_SUPPORTED and tunnels it to the remapped origin as the ATS uid.
That is the leftover. Cache PURGE/PUSH still need a well-known wksidx, so mixed-case does not purge or push the cache. Uppercase DELETE from the same sidecar is 403.
Why Traffic Server
I had just finished Spark leftovers on standalone REST and Connect archives, and a Tomcat RewriteValve loop. ATS is the other large ASF reverse proxy people put in front of origins and then trust ip_allow.yaml to mean what it says. HTTP/1.1, HTTP/2, remap, cache, JSON-RPC, a plugin zoo, Cripts, ESI. SECURITY.md is honest: administrative users and configuration files are trusted. Stock plugin.config is empty. remap_required defaults 1.
I only wanted Critical or High. Remote and local. Medium stays on the floor unless it chains. remap_required: 0 is a documented open relay. Lua os.execute is operator script. stats_over_http is metrics. Those wait.
Pinning 10.2.0, not a 10.1 backport
trafficserver.apache.org listed 10.2.0 as Latest Stable (17 Aug 2026). Tag object d89fdd257ea3e6cd0e70bcae9d36f3b7355af279. Peel landed on b1df7334b39a878971c376da849e8da4984918b6. CMake project(ats VERSION 10.2.0). I cloned that peel and built ats-10.2.0-lab:local from source. Master moves. 9.2.x is the other stable line and already had the July backports. I wanted the current 10.x tree.
The skip list is half the hunt
CHANGELOG-10.2.0 already ships the July 2026 wave, plus CVE-2026-65100, CVE-2026-65325, and CVE-2026-102795 (full-string SNI/Host compare). I wrote those down so I would not "discover" a CVE with a press release.
- CVE-2026-33267 hop-by-hop /
@headers.copy_header_fieldsstrips Connection-listed names.@stripped inModifyRequest. - CVE-2026-58150 HTTP/2 Transfer-Encoding smuggling. H2/H3 TE rejected.
- CVE-2026-58157 server-session reuse.
releaseSessionrefuses a non-empty origin reader. - CVE-2026-22068 unanchored regex ACL/SNI. Core
regex_mapisRE_ANCHORED | RE_ENDANCHORED. - CVE-2026-58156 URL/port parse ACL. String ports outside uint16 rejected.
- CVE-2026-58159 HTTP UDS listener ACL. No HTTP UDS on default
server_ports. - CVE-2026-58164 remap shutdown UAF. Null-check held.
- CVE-2026-102795 SNI to Host matching.
- CVE-2026-65100 HPACK encode-before-confirm.
- CVE-2026-65325 HTTP/2 multiplexed origin session cert reuse.
Named leftovers otherwise closed on this pin: inverted empty-nonstandard remap ACL, default CONNECT dest deny including 0.0.0.0/8 and ::ffff:0:0/96, ats_is_ip_any before dest ACL.
Twenty-four surfaces, then this door
I split the tree: HTTP/1.1 parser, HTTP/2, MIME/HdrHeap, TLS/SNI, remap/ACL, cache, DNS/HostDB, PROXY protocol, JSON-RPC, origin session reuse, ESI, header_rewrite, Lua, authproxy/certifier/url_sig, regex_remap, xdebug/stats, experimental plugins, Cripts, SSRF/redirects, UDS/http+unix, Range/slice/compress, parent/NextHop, logging, HTTP/3.
Most of that is ATS doing what ATS said it would do. JSON-RPC world-connectable UDS is a different leftover. regex_remap noslash $n is a different leftover. ESI unix recv-port is a different leftover. Lua fetch_multi UAF did not crash under ASan. I am not bundling them.
Default reverse-proxy 10.2.0 (empty plugin.config, remap_required: 1, at least one map) has no unpublished RCE. The method ACL is the first default remote door that still answered after the skip list.
YAML is nocase. HTTP/1 is not.
Default inbound deny (configs/ip_allow.yaml.default):
- apply: in
ip_addrs: 0/0
action: deny
methods:
- PURGE
- PUSH
- DELETE
- TRACEYAML load uses hdrtoken_tokenize (nocase) so those names become standard bits. DENY invert then sets _method_mask = ALL & ~listed and _deny_nonstandard_methods = true with an empty nonstandard list (IPAllow.cc:545-548).
HTTP/1 parse tokenizes the method with hdrtoken_method_tokenize (HTTP.cc). The DFA is case-insensitive. The helper then strncmps the request bytes against the well-known string (HdrToken.cc:512-528):
retval = hdrtoken_tokenize(string, string_len, &string_out);
if (retval >= 0) {
if (strncmp(string, string_out, string_len) != 0) {
// Not a case match
retval = -1;
}
}delete, DeLeTe, trace, purge, and push land with method_get_wksidx() == -1.
EndRemapRequest calls process_quick_http_filter with that wksidx. For method == -1 it asks isNonstandardMethodAllowed (IPAllow.h:365-376). Empty nonstandard set plus deny-nonstandard is !method_in_set, so mixed-case is allowed. A later is_this_http_method_supported(-1) is false. METHOD_NOT_SUPPORTED sets ProxyMode_t::TUNNELLING and returns true (HttpTransact.cc:6855-6858). Docs say ATS forwards unknown methods to the origin. Cache lookup is skipped in TUNNELLING.
HTTP/2 :method goes through HTTPHdr::method_set -> hdrtoken_tokenize with no case check. H2 delete becomes DELETE and is denied. This leftover is HTTP/1.
Remap @method=DELETE is the same split: config parse is nocase; match uses wksidx. H1 delete is wksidx -1. Default remap.config has no ACL filters. On a typical map with no @method, only ip_allow.yaml stands in the way.
The lab
Host is macOS. Source-built image ats-10.2.0-lab:local. Three containers, one compose project ats-ipallow-method-case. Loopback 127.0.0.1:18710 published to ATS 8080.
The attacker cannot be 127.0.0.1. Default inbound allow ALL on localhost. The PoC runs in a sidecar on 172.x and speaks raw HTTP/1 so urllib cannot canonicalize the method.
Origin folds method case. That is the class: IIS / some Java stacks honor mixed-case as the well-known verb. RFC 9110 §9.1 case-sensitive origins (nginx, httpd, Go net/http) answer 501. I am not filing "ATS forwarded an unknown method to nginx." I am filing that the deny list did not match.
CONTROL: uppercase DELETE and TRACE from the sidecar are 403. Origin did not see them. POC: delete, DeLeTe, and trace are 200. Origin wrote ATS-IPALLOW-METHOD-CASE-WITNESS / TRACE-OK. Mixed-case purge / push reached the origin as PURGE-ORIGIN / PUSH-ORIGIN. Cache ops stayed closed.
Last line:
SUCCESS ats-ipallow-method-case how=live control=DELETE/TRACE-403 poc=delete/DeLeTe/trace-tunneled cache-purge-push=closed ATS-IPALLOW-METHOD-CASE-WITNESSReplay against loopback. cd lab &&./run.sh. Bind it to loopback. The loopback client is lab/run.sh with a banner on it.
Wrong turns, in the order I made them
Curling from the host. Published 127.0.0.1:18710 is localhost ALL. The deny never runs. The sidecar exists so the 403 is real.
Treating RFC 9110 case-sensitive origins as making the deny list a no-op. ATS cache PURGE/PUSH stay closed because they need wksidx. Origin DELETE/TRACE is the leftover, and only against origins that fold case. The lab origin folds case on purpose.
Claiming cache purge. Mixed-case purge is an unknown method. It tunnels. It does not walk the cache delete path. push_method_enabled default 0.
Re-filing hop-by-hop. CVE-2026-33267 is closed. Connection-listed names are stripped. This is method case, not a header.
HTTP/2 mixed-case. :method is nocase. I spent a pass confirming H2 delete is 403. It is.
Inverted empty-nonstandard remap ACL. CHANGELOG already named that fix. Default map has no @method. ip_allow is the stock ACL.
A reverse shell. Theatre. The oracle is a witness string on the origin after uppercase 403.
What an attacker can do
Reach a typical reverse proxy (remap_required: 1 plus one map) on HTTP/1 from a non-loopback address. Empty plugin.config. Default ip_allow.yaml.
| Method | Sink | Privilege |
|---|---|---|
HTTP/1 delete / DeLeTe after remap from a 172.x sidecar |
hdrtoken_method_tokenize then TUNNEL (HdrToken.cc:512-528, HttpTransact.cc:6855-6858) |
Integrity (lab-proven): origin 200 DELETED ATS-IPALLOW-METHOD-CASE-WITNESS. Uppercase DELETE is 403. Origin method as ATS uid. |
HTTP/1 trace |
same TUNNEL | Recon (lab-proven): origin 200 TRACE-OK. ATS does not build the TRACE echo. Uppercase TRACE is 403. |
HTTP/1 purge / push |
unknown-method TUNNEL, not cache op | Integrity (ATS cache): closed (lab-proven). Origin saw PURGE-ORIGIN / PUSH-ORIGIN. Cache ops need well-known wksidx. |
| Same mixed-case request | one extra origin hop | Availability (lab-proven): not process death. |
Lab-proven vs inferred: uppercase deny, mixed-case DELETE/TRACE tunnel, cache PURGE/PUSH closed: lab. Origin DELETE/TRACE only if that origin folds method case.
Requires: HTTP/1 from a non-loopback client. remap_required: 1 plus one map. Default inbound deny of PURGE/PUSH/DELETE/TRACE.
You get the origin method as the ATS uid, not a cache wipe, not HTTP/2, not CONNECT to RFC1918. You do not get a new hop-by-hop smuggle.
Adjacent surface that stays closed
| Surface | Lab |
|---|---|
Exact-case DELETE / TRACE from 172.x |
403, origin not hit |
HTTP/2 :method mixed-case |
nocase tokenize, still denied |
Mixed-case purge / push as ATS cache ops |
origin saw the verb; cache path stayed closed |
| CONNECT dest deny | default ip_allow.yaml outbound |
remap_required: 1 miss |
404, not an open relay |
| Inverted empty-nonstandard remap ACL | CHANGELOG fix held |
CVE-2026-33267 hop-by-hop / @ strip |
closed on this pin |
The fix
Tokenize HTTP/1 methods the same way YAML and HTTP/2 already do (nocase), or match ip_allow.yaml deny names nocase before the unknown-method TUNNEL. Keep cache PURGE/PUSH on well-known wksidx. Do not treat a case variant of a denied well-known method as a new method.
Until then, a reverse proxy that believes default ip_allow.yaml blocks DELETE/TRACE from the internet still forwards delete and trace to the origin.
I am not going to print a raw request-line you can paste at someone else's :8080. The case split is the useful part. If you own the proxy, run the proof of concept against loopback.
References
- Proof of concept: abraxas/ats-ipallow-method-case · ats-ipallow-method-case-Abraxas-Labs.py
- Lab:
lab/· Dockerfile · docker-compose.yml · run.sh · poc.py - @abraxas_null · github.com/abraxas · abraxaslabs.tech · abraxas.null@proton.me
- CWE-178 · CWE-863
- Tree: apache/trafficserver 10.2.0 (
b1df7334b39a878971c376da849e8da4984918b6) ·HdrToken.cc·IPAllow.cc·IPAllow.h·HttpTransact.cc·ip_allow.yaml.default