ATS 10.2.0 ESI FetchSM port 0 matches http+unix remap
Apache Traffic Server 10.2.0 ESI FetchSM PluginVC reports dest port 0, so an http include matches map_with_recv_port http+unix rules meant only for UDS listeners. TCP /secret stays TCP. CONTROL without that map has no witness. Labbed. No CVE yet.
- Name
- ATS 10.2.0 ESI FetchSM port 0 matches http+unix remap
- 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) ESI plus map_with_recv_port http+unix. Typical esi.so plus one map does not hit this.
- Published
- 13 Oct 2026
- Updated
- 14 Oct 2026
- Tags
- 0-day, trafficserver, esi, ssrf, remote, cwe-918
Port 0 meant unix. The include was still http.
The proof of concept is on GitHub: abraxas/ats-esi-unix-recvport (loopback client; @abraxas_null). The lab stack is lab/: Dockerfile, docker-compose.yml, run.sh, poc.py, origin.py, etc-poc/remap.config, etc-control/remap.config. Authorized lab only. It talks to loopback.
This is Apache Traffic Server 10.2.0 (b1df7334b39a878971c376da849e8da4984918b6). No CVE yet. CWE-918. 8.6 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N). Remote SSRF. ESI plugin plus map_with_recv_port http+unix. Not typical esi.so plus one map.
ESI FetchSM PluginVC reports dest port 0. Remap treats port 0 as UDS and rewrites the include scheme to http+unix so it matches map_with_recv_port http+unix:// rules meant only for UDS listeners. A public-looking esi:include of http://www.example.com/secret then hits the unix-socket origin.
The July 2026 validator rejects include scheme http+unix as BAD_SCHEME. It does not reject an http:// include whose internal remap hop became unix because dest port was 0. TCP GET /secret stays on the TCP map. CONTROL without the unix recv-port line inlines the TCP origin. No witness.
Why ESI after regex_remap
regex_remap noslash $n is one SSRF. ESI is the other plugin people enable because the origin already emits esi:include. CVE-2026-58178 already taught this plugin a lesson: private literals, file://, nested comments, depth cap. I wanted to know what the validator still trusts once those are closed.
I only wanted Critical or High. Hostname denylist without DNS is documented and dead on remap_required: 1. Location follow needs number_of_redirections > 0 (default 0). Typical esi.so plus one map is not this leftover. CONTROL exists so I cannot pretend it is.
Pinning 10.2.0
Same peel: tag 10.2.0, commit b1df7334b39a878971c376da849e8da4984918b6, image ats-10.2.0-lab:local. number_of_redirections stayed 0. remap_required stayed 1. Include scheme http+unix stayed BAD_SCHEME. The leftover is recv-port 0, not those.
The skip list, for this plugin
- CVE-2026-58178 ESI unbounded recursion / SSRF. Validator on every production
EsiProcessor.file://,http+unixas an include scheme, IPv4-mapped / NAT64, decimal/octal/hex IPv4, zone ids,localhost, 169.254 literals: rejected. Nested<!--esi -->raw-byte reject. Depth cap 3/9. - CVE-2025-49763 ESI memory exhaustion. Skip.
- CVE-2026-58189 plugin redirect-counter. Closed. Default follow is still 0.
- Hostname denylist without DNS. Documented. Unmapped host 404s when remap_required=1.
FetchSM dest port is zero
Includes are not a raw socket to src. HttpDataFetcherImpl::addFetchRequest builds GET <url> HTTP/1.0 and calls TSFetchUrl. FetchSM::httpConnect uses PluginHttpConnectInternal. That is an internal ATS transaction: remap, cache, origin. ESI does not set TS_FETCH_FLAGS_SKIP_REMAP.
PluginVCCore zeros passive_addr_struct (PluginVC.h:274-278). PluginHttpConnectInternal sets only active_addr (the original client). FetchSM's HttpSM runs on the passive PluginVC. attach_client_session copies netvc->get_local_addr() into client_info.dst_addr. Passive local addr is the zeroed struct. host_order_port() is 0.
Remap lookup (RemapProcessor.cc:87-114):
if (s->client_info.dst_addr.host_order_port() == 0) {
// Port number 0 means that UDS is used.
// Adjust the scheme so that we can find rules with +unix suffix.
...
if (auto scheme = adjusted_url.scheme_get_wksidx(); scheme == URL_WKSIDX_HTTP) {
adjusted_url.scheme_set("http+unix");
}Unit test test_RemapRules.cc:237-240 documents that. 10.2 added http+unix recv-port maps for UDS listeners. FetchSM was already port 0. Those two facts meet.
Poc remap:
map http://www.example.com/ http://origin-tcp:8080/
map_with_recv_port http+unix://www.example.com/ http://origin-uds:8080/Control remap is the first line only.
The lab
Four containers: origin-tcp, origin-uds, ats-control (127.0.0.1:18751), ats-poc (127.0.0.1:18750). Compose project ats-esi-unix-recvport. Image ats-10.2.0-lab:local. Shared UDS volume. esi.so on both ATS instances.
TCP origin /page is an ESI document: esi:include src="http://www.example.com/secret" plus X-Esi: 1. TCP /secret is TCP-ORIGIN-OK. UDS /secret is ATS-ESI-UNIX-RECVPORT-WITNESS.
CONTROL GET /page: 200, inlines TCP-ORIGIN-OK, no witness. POC GET /secret as a TCP client: 200, TCP-ORIGIN-OK, no witness. POC GET /page: 200, inlined witness, no TCP body.
Last line:
SUCCESS ats-esi-unix-recvport control-no-witness=yes poc-page-witness=yes poc-tcp-secret-no-witness=yes ATS-ESI-UNIX-RECVPORT-WITNESSReplay against loopback. cd lab &&./run.sh.
Wrong turns, in the order I made them
Treating typical esi.so + one map as this leftover. CONTROL is that config. No witness. The unix recv-port line is the gate.
Filing hostname denylist without DNS. Documented. Dead on remap_required=1. Not this post.
Claiming file:// still works. Validator BAD_SCHEME / MALFORMED. CVE-2026-58178.
Expecting TCP /secret to follow the unix map. Recv-port match is FetchSM dest port 0. A real TCP client has a real dest port. Stayed on the TCP map. That is why the SUCCESS line has three clauses.
Include scheme http+unix. Validator rejects it. The leftover is an http:// include whose internal hop was rewritten.
A reverse shell. Theatre. The oracle is an inlined witness from the UDS origin, with TCP /secret still TCP.
What an attacker can do
Unauth HTTP to an ESI vhost whose remap has map_with_recv_port http+unix://. Include src the attacker can influence (compromised origin, stored ESI, or $(HTTP_HEADER{...})). The lab include is a static http://www.example.com/secret on /page.
| Method | Sink | Privilege |
|---|---|---|
Unauth GET /page ESI include http://www.example.com/secret |
FetchSM dest port 0 matches map_with_recv_port http+unix://www.example.com/ (RemapProcessor.cc:87-114) |
Recon / SSRF (lab-proven): client body inlined ATS-ESI-UNIX-RECVPORT-WITNESS from the UDS origin. |
| Same | first-order write / shell | Integrity: none. No first-order write. No shell. |
CONTROL GET /page without unix recv-port map |
TCP origin include | Closed (lab-proven): inlines TCP-ORIGIN-OK, no witness. |
TCP client GET /secret on the poc vhost |
TCP map | Closed (lab-proven): TCP-ORIGIN-OK, no witness. |
Lab-proven vs inferred: control no witness, poc page witness, poc TCP /secret no witness: lab. Header-expansion of src is the documented ESI trust model, not re-proven here.
Requires: ESI plugin plus map_with_recv_port http+unix. Default remap_required: 1. number_of_redirections: 0.
You get ATS as a client to a UDS origin that was supposed to be reachable only from a unix listener, not a TCP /secret bypass, not file://, not a typical esi.so enable. Scope changes because the include left the TCP origin.
Adjacent surface that stays closed
| Surface | Lab |
|---|---|
| CONTROL without unix recv-port map | control-no-witness=yes |
TCP client GET /secret |
poc-tcp-secret-no-witness=yes |
Include scheme http+unix |
validator BAD_SCHEME |
file://, private literals, nested comments, depth cap |
CVE-2026-58178 closed |
| Hostname denylist without DNS | documented; dead on remap_required: 1 |
number_of_redirections: 0 |
Location follow off |
The fix
Do not treat FetchSM PluginVC dest port 0 as a UDS recv-port match. Keep map_with_recv_port http+unix:// for real UDS listeners. ESI includes of http://host/ should stay on the TCP map even when an http+unix recv-port rule exists for that host.
Until then, a 10.2 unix recv-port rule next to ESI is an include trampoline onto that UDS origin.
I am not going to print an ESI include you can paste at someone else's vhost. The port-0 rewrite is the useful part. If you own the proxy, run the proof of concept against loopback.
References
- Proof of concept: abraxas/ats-esi-unix-recvport · ats-esi-unix-recvport-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-918
- Tree: apache/trafficserver 10.2.0 (
b1df7334b39a878971c376da849e8da4984918b6) ·PluginVC.h·PluginHttpConnect.cc·RemapProcessor.cc·test_RemapRules.cc