Research/ats-ipallow-method-case
0-dayNo CVEHighPublic

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_fields strips Connection-listed names. @ stripped in ModifyRequest.
  • CVE-2026-58150 HTTP/2 Transfer-Encoding smuggling. H2/H3 TE rejected.
  • CVE-2026-58157 server-session reuse. releaseSession refuses a non-empty origin reader.
  • CVE-2026-22068 unanchored regex ACL/SNI. Core regex_map is RE_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):

YAML
- apply: in
 ip_addrs: 0/0
 action: deny
 methods:
 - PURGE
 - PUSH
 - DELETE
 - TRACE

YAML 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):

C
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:

Plain text
SUCCESS ats-ipallow-method-case how=live control=DELETE/TRACE-403 poc=delete/DeLeTe/trace-tunneled cache-purge-push=closed ATS-IPALLOW-METHOD-CASE-WITNESS

Replay 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