ATS realip PLUGIN ACL fail-open on remap @src_ip
Apache Traffic Server 10.2.0 experimental realip plus acl.subjects=PLUGIN fail-opens remap @src_ip. Unverified addr stays AF_UNSPEC. PLUGIN is never nullptr, so PEER fallback never runs. A remap allow-list matches the untrusted client. Labbed. CONTROL PEER 403. Session PURGE still 403.
- Name
- ATS realip PLUGIN ACL fail-open on remap @src_ip
- 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) experimental realip.so with acl.subjects PLUGIN (or PLUGIN,PEER). Default subjects is PEER. Not default.
- Published
- 13 Oct 2026
- Updated
- 14 Oct 2026
- Tags
- 0-day, trafficserver, realip, privilege-leftover, cwe-863
PLUGIN was never nullptr. PEER never got a vote.
I am @abraxas_null. The proof of concept is on GitHub: abraxas/ats-realip-plugin-acl (loopback client). The lab stack is lab/: Dockerfile, docker-compose.yml, run.sh, poc.py, sidecar_http.py. Authorized lab only. It talks to loopback.
This is Apache Traffic Server 10.2.0 (b1df7334b39a878971c376da849e8da4984918b6). No CVE yet. CWE-863. 8.1 High (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H). Remote privilege leftover. experimental realip plus the documented acl.subjects: PLUGIN. PR:N from an address outside trustedAddress.
The admin guide tells you to load realip.so and set proxy.config.acl.subjects to PLUGIN so remap @src_ip sees the address behind the trusted proxy. That is the feature. The leftover is what happens when realip does not verify you.
_verified_addr stays the zeroed sockaddr_storage (AF_UNSPEC). get_verified_client_addr always returns that storage, never nullptr. PerformACLFiltering treats PLUGIN as present and breaks, so the "first available" fallback to PEER never runs. ats_is_ip is false for AF_UNSPEC, so @src_ip is skipped and ip_matches stays true. A remap allow-list "only 10.0.0.0/8" becomes an allow for a client who skipped the trusted proxy.
PLUGIN,PEER has the same hole: PLUGIN is never unavailable.
Why Traffic Server
I wanted a reverse proxy that still has a 2026 ACL story. ATS shipped PROXY-protocol subject, then PLUGIN subject, then a new 10.2 experimental realip plugin whose entire job is to fill that PLUGIN slot. The docs show acl.subjects: PLUGIN next to a trustedAddress CIDR. That is the setup people will copy.
I only wanted Critical or High. Remote preferred. Experimental plugins are in scope when a leftover is serious. SECURITY.md still treats them as unstable. Fail-open remap ACL on the documented enable path is serious.
Stock 10.2.0 does not load realip. Default acl.subjects is PEER. Classic X-Forwarded-For-from-anyone is closed: trustedAddress is required, and the header must be a single inet_pton address. I am not filing "operator trusted 0.0.0.0/0". I am filing "operator followed the guide, client is not in trustedAddress, remap @src_ip still matches."
Pinning 10.2.0
Latest Stable 10.2.0, peel b1df7334b39a878971c376da849e8da4984918b6. realip is new in this line. I cloned that peel and built ats-10.2.0-lab:local from it. -DBUILD_EXPERIMENTAL_PLUGINS=ON. Loopback only.
The skip list is half the hunt
July 2026 already named experimental memory/limit, txn_box stack, uri_signing/url_sig, intercept UAF, webp_transform, multiplexer chunks. PROXY-protocol allowlist is CVE-2025-31698 class, already on this pin. Session ip_allow.yaml never consults PLUGIN. SNI ip_allow is PEER/PROXY only, handshake before HTTP headers.
I dropped wasm WASI (path_open NOENT), inliner #inline PluginVC, ICAP RESPMOD-only, cookie_remap YAML, tls_bridge peer list. Those are operator config or already-public classes.
The two experimental leftovers that stayed were this PLUGIN fail-open and unsigned magick convert. Magick is a different post.
How PLUGIN becomes "present"
AddressSetter is TS_HTTP_READ_REQUEST_HDR_HOOK. verify is trustedAddress.contains(client addr). Empty set contains nobody. Client outside the CIDR: verify false, TSHttpTxnVerifiedAddrSet skipped (address_setter.cc:29-38).
ProxyTransaction::_verified_addr is a sockaddr_storage{}. Getter:
return reinterpret_cast<const struct sockaddr *>(&_verified_addr);Never nullptr. TSHttpTxnVerifiedAddrGet returns TS_ERROR on AF_UNSPEC. Remap does not use the getter. It uses the raw pointer.
UrlRewrite::PerformACLFiltering:
} else if (IpAllow::Subject::PLUGIN == IpAllow::subjects[i]) {
src_addr =... get_verified_client_addr();
break;
}
if (src_addr == nullptr) {
// fallback to PEER
}PLUGIN always takes the break. PEER fallback is dead. Then:
bool ip_matches = true;
if (ats_is_ip(src_addr)) {
// @src_ip check
}AF_UNSPEC is not an IP. The check is skipped. ip_matches stays true. Allow-list matches.
Connection accept already ran IpAllow::match on the peer. PLUGIN cannot rewrite that. Attacker PURGE is still 403 on default ip_allow.yaml. The leftover is remap ACL, not session ACL.
Wrong turns, in order
Filing "trust XFF from anyone". Simple source fail-closes when trustedAddress is empty or the client is outside it. XFF lists fail inet_pton of the whole field. Header from an untrusted peer is ignored. Same fail-open as missing header, not a spoof of the verified address.
Claiming session ip_allow.yaml is bypassed. It is not. Lab PURGE 403. PLUGIN omitted from HttpSessionAccept.
Default subjects. PEER. realip loaded but subjects left at PEER: verified addr is logging / header_rewrite PLUGIN source only. Remap ACL still uses the peer. CONTROL in the lab is that stack: 403.
Empty trustedAddress as a separate CVE. That is fail-closed for setting the address, fail-open for PLUGIN remap ACL once subjects is PLUGIN. Same bug. One report.
A reverse shell. Theatre. The oracle is origin 200 ATS-REALIP-PLUGIN-ACL-WITNESS against CONTROL 403.
The lab
Loopback 127.0.0.1:18770. Origin 18771. Compose ats-realip-plugin-acl. Two ATS containers: PLUGIN subjects vs PEER subjects. Same remap @src_ip=10.0.0.0/8 allow on internal.lab. Attacker sidecar 172.x, not in trustedAddress (172.30.70.30 is the trusted one).
CONTROL PEER: 403. POC PLUGIN: 200 plus witness. Untrusted Client-IP: 10.1.2.3: still 200. PURGE: 403. Host denied.lab without @src_ip: body DENIED. Trusted sidecar with Client-IP: 10.1.2.3: 200 (intended).
Last line:
SUCCESS ats-realip-plugin-acl control-http=403 poc-http=200 purge-http=403 denied-http=200 spoof-http=200 trusted-http=200 ATS-REALIP-PLUGIN-ACL-WITNESSWhat an attacker can do
Reach ATS with experimental realip.so loaded and acl.subjects: PLUGIN (or PLUGIN,PEER). Remap @src_ip=10.0.0.0/8 allow. Connect from an address outside trustedAddress.
| Method | Sink | Privilege |
|---|---|---|
HTTP from 172.x outside trustedAddress to remap @src_ip=10.0.0.0/8 |
realip skip TSHttpTxnVerifiedAddrSet (address_setter.cc:29-38); PLUGIN subject always present (UrlRewrite.cc:462-473); skip @src_ip when !ats_is_ip (:495-514) |
Integrity (lab-proven): remap ACL matches. Origin 200 ATS-REALIP-PLUGIN-ACL-WITNESS. CONTROL acl.subjects=PEER is 403. |
Same plus Client-IP: 10.1.2.3 from the untrusted peer |
simple.cc verify false; header ignored |
Integrity (lab-proven): still fail-open 200. Not a spoof of the verified address. |
Same client PURGE |
session ip_allow.yaml PEER |
Closed (lab-proven): 403. PLUGIN never rewrites connection ACL. |
Host without @src_ip |
default map | Closed (lab-proven): body DENIED, no witness. |
Lab-proven vs inferred: PLUGIN 200, PEER 403, PURGE 403, denied map, untrusted header fail-open are lab. What the origin then grants is inferred from the remap that was supposed to be 10/8-only.
Requires: experimental realip.so plus acl.subjects=PLUGIN (or PLUGIN,PEER). Remap @src_ip allow-list. Not default.
You get the remap that was meant for 10/8. You do not get session PURGE/PUSH. You do not spoof the verified address from an untrusted peer. You do not skip SNI ip_allow. You do not get this on default PEER.
Adjacent surface that stays closed
| Surface | Why |
|---|---|
CONTROL acl.subjects=PEER |
403 on the same @src_ip map |
Session ip_allow.yaml |
attacker PURGE 403 |
Map without @src_ip |
DENIED |
Empty trustedAddress |
never verifies; leftover only when subjects is PLUGIN |
| Classic XFF-from-anyone | closed |
SNI ip_allow |
PEER/PROXY only |
Default subjects PEER |
leftover does not apply |
The fix
Treat PLUGIN as unavailable when get_verified_client_addr is AF_UNSPEC (or make the getter return nullptr, matching TSHttpTxnVerifiedAddrGet). Then PEER fallback runs. Do not skip @src_ip when the subject pointer is non-null but not an IP. Fail closed on an allow-list.
Until then, leave acl.subjects at PEER if you load realip only for logging. Do not set PLUGIN unless every client that can hit the remap is already a trusted proxy.
I am not going to print a Host header you can paste at someone else's cache. The missing nullptr is the useful part. If you own the proxy, run the proof of concept against loopback.
References
- Proof of concept: abraxas/ats-realip-plugin-acl · ats-realip-plugin-acl-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-863
- Tree: apache/trafficserver 10.2.0 (
b1df7334b39a878971c376da849e8da4984918b6) ·address_setter.cc·UrlRewrite.cc·ProxyTransaction.h - realip admin guide