ATS 10.2.0 JSON-RPC UDS defaults unrestricted 0777
Apache Traffic Server 10.2.0 JSON-RPC unix socket defaults to restricted_api false and Linux mode 0777. A local uid dumps live records including SSL key paths and connection-tracker tuples. Writes stay behind SO_PEERCRED. Labbed. No CVE yet.
- Name
- ATS 10.2.0 JSON-RPC UDS defaults unrestricted 0777
- 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 JSON-RPC unix. Local uid on the ATS host. Docs table says true/0700; code is false/0777.
- Published
- 13 Oct 2026
- Updated
- 14 Oct 2026
- Tags
- 0-day, trafficserver, jsonrpc, privilege-leftover, local, cwe-276, cwe-284
The docs said 0700. The socket was 0777.
The proof of concept is on GitHub: abraxas/ats-jsonrpc-unrestricted (loopback client; @abraxas_null). The lab stack is lab/: Dockerfile, docker-compose.yml, run.sh, poc.py, jsonrpc.yaml, entrypoint.sh. Authorized lab only. It talks to loopback.
This is Apache Traffic Server 10.2.0 (b1df7334b39a878971c376da849e8da4984918b6). No CVE yet. CWE-276 / CWE-284. 7.1 High (CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N). Local privilege leftover (recon). Not RCE. Not config write from a foreign uid.
JSON-RPC is on unless you turn it off. The only transport is a unix socket. Shipped jsonrpc.yaml is comments only, so the C++ initializer wins. That initializer is NON_RESTRICTED_API (false). On Linux, SO_PEERCRED is present, so bind mode is 0777. Any local uid that can reach the rundir inode connects with no token and dumps live records, including SSL key paths, plus connection-tracker 5-tuples.
Writes stay behind SO_PEERCRED. uid 10000 admin_config_set_records is Unauthorized. Docs table last sentence says true by default and 0700. Code and the commented example follow false. That disagreement is the leftover.
Why this socket, after the method ACL
Default reverse-proxy 10.2.0 has no unpublished RCE. Mixed-case HTTP/1 is the remote default door. JSON-RPC is the local one. SECURITY.md trusts administrative users. A world-connectable management socket with unrestricted reads is not that trust. It is every uid on the box.
I only wanted Critical or High. Local is in-scope. A 96KiB parse as local DoS is Medium. Same-uid traffic_ctl is intended admin. TCP JSON-RPC does not exist.
Pinning 10.2.0
Same peel as the method-case leftover: tag 10.2.0, commit b1df7334b39a878971c376da849e8da4984918b6, image ats-10.2.0-lab:local. CHANGELOG already named mgmt/rpc: create JSONRPC unix socket with intended mode atomically. That fix is the restricted path. Unrestricted bind still chmod 0777 after bind. I wanted to know which path was default.
The skip list, for this surface
- CVE-2026-58159 HTTP UDS listener ACL. No HTTP UDS on default
server_ports. Mgmt JSON-RPC is a different inode. - Atomic umask+bind when
restricted_apiis true. 0700. Held. - Old traffic_manager XMLRPC TCP. Gone. Only
CommType::UNIXexists.RPCServerthrows on anything else.
July smuggling / SNI / HPACK / ESI guards are closed. They are not this socket.
Default bind is the docs table inverted
RPC is on unless rpc.enabled: false. Missing jsonrpc.yaml keeps that default. Default inode: RecConfigReadRuntimeDir() + jsonrpc20.sock. Runtime dir is typically 0755 ATS-owned: other uids cannot replace the socket, they can connect if the socket is 0777.
Shipped jsonrpc.yaml.default is comments only:
# rpc:
# enabled: true
# unix:
# restricted_api: falseYAML decode only writes restrictedAccessApi when the key is present. Otherwise (IPCSocketServer.h:128-129):
bool restrictedAccessApi{
NON_RESTRICTED_API}; //... 0700(default) or 0777NON_RESTRICTED_API is false. Comment says 0700 is default. Initializer is the unrestricted constant.
Bind (IPCSocketServer.cc:310-342):
restrictedAccessApi == falseandhas_peereid(): mode 00777.restrictedAccessApi == falseand no peereid: forced restricted, mode 00700.restrictedAccessApi == true: mode 00700.
Linux has TS_HAS_SO_PEERCRED. Default install: mode 0777.
Docs (jsonrpc.yaml.en.rst) table text says non-root read-only JSON-RPC when false, writes still root/same-uid, then the last sentence of the same row says true by default and 0700 vs 0777. Unit test only asserts restricted_api: true yields 0700. Restore YAML omits the key. There is no test that the omitted key is 0700.
Authn on connect: none. Handler gate late_check_peer_credentials only if options.auth.restricted. Then getpeereid / SO_PEERCRED. Allow if euid == 0 or euid == geteuid(). gid is never checked.
The lab
One container. Compose project ats-jsonrpc-unrestricted. Loopback HTTP 127.0.0.1:18720 so the stack is alive. The leftover is the unix socket inside the container, not that HTTP port.
Lab user uid 10000. ATS uid nobody. Shipped commented jsonrpc.yaml. traffic_ctl as the ATS user plants proxy.config.proxy_name=ATS-JSONRPC-UNRESTRICTED-WITNESS. uid 10000 then speaks JSON-RPC 2.0 on jsonrpc20.sock.
stat of the socket: mode 777. admin_lookup_records returned the witness name, SSL server/client private-key paths under etc/trafficserver, and proxy.config.admin.user_id=nobody. Key bytes are not in records. get_connection_tracker_info returned inbound/outbound tables. admin_config_set_records as uid 10000: error 10 Unauthorized, Denied privileged API access for uid=10000. Witness record unchanged. /proc/net/tcp had 8080 and 18080. No JSON-RPC TCP listen.
Last line:
SUCCESS ats-jsonrpc-unrestricted how=uds-0777-unrestricted-jsonrpc uid=10000 sock-mode=0777 lookup-proxy_name=ATS-JSONRPC-UNRESTRICTED-WITNESS ssl-key-path=etc/trafficserver tracker=ok write=unauthorized tcp-jsonrpc=absent ATS-JSONRPC-UNRESTRICTED-WITNESSReplay against loopback. cd lab &&./run.sh.
Wrong turns, in the order I made them
Believing the docs table. The last sentence says true / 0700. The initializer is false. I almost skipped the surface as "intended restricted."
Calling this RCE. Writes are SO_PEERCRED. uid 10000 cannot set records, reload plugins, or drain. Class is recon. Inflating it to LPE is wrong.
Treating CVE-2026-58159 as covering mgmt JSON-RPC. That CVE is the HTTP UDS listener ACL. This inode is jsonrpc20.sock. Different bind, different methods.
Looking for a TCP leftover of traffic_manager. Only UNIX exists. I grepped /proc/net/tcp anyway. HTTP 8080 / origin 18080. No JSON-RPC port.
Network FS that ignores umask. Below bar. Default local Linux FS honors bind+chmod.
A reverse shell. Theatre. The oracle is a planted proxy_name plus SSL key path strings, then a denied set.
What an attacker can do
Local uid on the ATS host (not root, not ATS euid). RPC enabled. JSON-RPC unix on. Typical rundir 0755 ATS-owned. Socket $RUNTIMEDIR/jsonrpc20.sock.
| Method | Sink | Privilege |
|---|---|---|
admin_lookup_records as uid 10000 on 0777 jsonrpc20.sock |
unrestricted JSON-RPC (RpcAdminPubHandlers.cc) |
Recon (lab-proven): live proxy.config.proxy_name=ATS-JSONRPC-UNRESTRICTED-WITNESS, SSL key paths etc/trafficserver, proxy.config.admin.user_id=nobody. Key bytes are not in records. |
get_connection_tracker_info |
same socket | Recon (lab-proven): inbound/outbound tracker tables. |
get_server_status, admin_host_get_status, admin_storage_get_device_status, filemanager.get_files_registry |
same unrestricted bit | Recon (inferred): drain state, host markdown, cache disk paths, absolute config paths. |
admin_config_set_records as uid 10000 |
SO_PEERCRED restricted | Integrity: closed (lab-proven). Unauthorized. Witness record unchanged. |
| drain / cache clear / storage offline | restricted methods | Availability: closed for a foreign uid. |
Lab-proven vs inferred: 0777 socket, uid 10000 lookup, tracker, write denied, TCP JSON-RPC absent: lab. Other unrestricted read methods: inferred from the same gate.
Requires: local uid on the ATS host. JSON-RPC unix on (default). Linux SO_PEERCRED so unrestricted bind is 0777.
You get live config values and tracker tuples, not a write, not plugin load, not a TCP management port. You do get SSL key paths. You do not get key bytes from this method.
Adjacent surface that stays closed
| Surface | Lab |
|---|---|
admin_config_set_records as uid 10000 |
Unauthorized; witness record unchanged |
| TCP / HTTP JSON-RPC | absent; HTTP 8080 / 18080 only |
Plugin .so load via RPC |
SO_PEERCRED |
Same-uid traffic_ctl |
intended admin |
Atomic umask+bind when restricted_api is true |
0700, CHANGELOG fix held |
| CVE-2026-58159 HTTP UDS listener ACL | not this socket |
The fix
Default restricted_api to true and bind 0700, matching the docs table. Align the commented jsonrpc.yaml example. Keep unrestricted reads behind an explicit opt-in. Writes already require SO_PEERCRED; do not open them.
Until then, every local uid that can see $RUNTIMEDIR can read the management API the docs said was 0700.
I am not going to print a unix-socket JSON-RPC client you can paste at someone else's rundir. The default false is the useful part. If you own the host, run the proof of concept against loopback.
References
- Proof of concept: abraxas/ats-jsonrpc-unrestricted · ats-jsonrpc-unrestricted-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-276 · CWE-284
- Tree: apache/trafficserver 10.2.0 (
b1df7334b39a878971c376da849e8da4984918b6) ·IPCSocketServer.h·IPCSocketServer.cc·RpcAdminPubHandlers.cc·jsonrpc.yaml.default