Research/ats-realip-plugin-acl
0-dayNo CVEHighPublic

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:

Plain text
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:

Plain text
} 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:

Plain text
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:

Plain text
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-WITNESS

What 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