libcurl reuses a learned RTSP Session header across different hosts on the same easy handle, enabling cross-host session leak and replay

Disclosed: 2026-04-18 19:37:36 By skksndk To curl
Medium
Vulnerability Details
## Summary: libcurl automatically learns RTSP `Session:` headers from server responses and stores them in `data->set.str[STRING_RTSP_SESSION_ID]` in `lib/rtsp.c:1015-1033`. On later RTSP requests using the same easy handle, `rtsp_do()` reads that easy-handle-scoped value at `lib/rtsp.c:373` and unconditionally emits `Session: %s` into the next request at `lib/rtsp.c:513-515`, without any host, port, or origin boundary check. Because the value lives in `data->set` / `struct UserDefined` rather than request-scoped state (`lib/urldata.h:878-883`, `lib/urldata.h:1143`, `lib/urldata.h:1405-1406`), it survives across subsequent RTSP requests to different hosts until manually cleared with `CURLOPT_RTSP_SESSION_ID = NULL` (`lib/setopt.c:2346-2351`) or the easy handle is destroyed (`lib/url.c:156-164`). In the attached PoC, request 1 to a victim RTSP server learns `Session: VICTIM-SESSION-123456`, request 2 to an attacker RTSP server on the same easy handle leaks that value, and a fresh handle can then replay the leaked session back to the victim with `PLAY` successfully accepted. A new-easy-handle control and a manual-clear control both suppress the leak. I reproduced this on curl 8.19.0 / libcurl 8.19.0 on x86_64 Linux and attached a zip with the PoC, logs, and a line-level root-cause note. ## Affected version Reproduced on: `curl 8.19.0 (x86_64-pc-linux-gnu) libcurl/8.19.0 OpenSSL/3.5.5 zlib/1.3.1 brotli/1.2.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.5 libssh2/1.11.1 nghttp2/1.68.1 ngtcp2/1.21.0 nghttp3/1.15.0 librtmp/2.3 mit-krb5/1.22.1 OpenLDAP/2.6.10` Platform: `Linux x86_64` ## Steps To Reproduce: 1. Start the local RTSP harness from the attached PoC directory: `python3 -u rtsp_session_leak_server.py` 2. Run the attached PoC: `python3 poc_rtsp_session_leak.py` 3. Observe the base case in the client output: - request 1: `SETUP rtsp://127.0.0.1:18201/media` returns `Session: VICTIM-SESSION-123456` - request 2: `OPTIONS rtsp://127.0.0.1:18202/evil` on the same easy handle sends `Session: VICTIM-SESSION-123456` 4. Observe the controls: - with a new easy handle for the attacker request, no `Session:` header is sent - after clearing `CURLOPT_RTSP_SESSION_ID` on the reused handle, no `Session:` header is sent 5. Observe the replay control: - a fresh handle sending `PLAY` to the victim without a session gets `RTSP/1.0 454 Session Not Found` - a fresh handle sending `PLAY` to the victim with the leaked `Session: VICTIM-SESSION-123456` gets `RTSP/1.0 200 OK` 6. Observe the attached sink-side server log: - attacker receives `{"role":"attacker","method":"OPTIONS","session":"VICTIM-SESSION-123456",...}` - victim then receives `{"role":"victim","method":"PLAY","session":"VICTIM-SESSION-123456","session_ok":true,...}` ## Line-Level Root Cause - The RTSP session identifier is automatically learned from server responses and stored on the easy handle. - `lib/rtsp.c:1015-1033` parses the `Session:` header from RTSP responses. - If `data->set.str[STRING_RTSP_SESSION_ID]` is not already set, `lib/rtsp.c:1031` stores the received value there. - `STRING_RTSP_SESSION_ID` is defined in `lib/urldata.h:955`. - That storage is easy-handle scoped, not request scoped. - The `UserDefined` structure in `lib/urldata.h:878-883` is documented as data set once and reused across independent connections. - `data->set` is the easy handle’s `struct UserDefined` in `lib/urldata.h:1405-1406`. - The backing string array is `lib/urldata.h:1143`: `char *str[STRING_LAST];` - The stored `Session:` value is then automatically attached to later RTSP requests. - `lib/rtsp.c:373` loads `p_session_id = data->set.str[STRING_RTSP_SESSION_ID];` - `lib/rtsp.c:513-515` emits `Session: %s` whenever that value is present. - There is no host, port, or origin boundary check between the read at line 373 and the emitted header at line 514. - The state is not automatically cleared when the RTSP target changes. - The explicit setter/clear path is `lib/setopt.c:2346-2351` via `CURLOPT_RTSP_SESSION_ID`, so applications can clear it manually by setting it to `NULL`, but libcurl does not clear it automatically when switching to a different RTSP host. - The public getter in `lib/getinfo.c:159-161` reads the same field, confirming that the “most recent” RTSP session ID is stored on the handle. - Generic cleanup only happens when the easy handle is destroyed in `lib/url.c:156-164`, where `Curl_freeset()` frees `data->set.str[i]`. - In this tree, references to `STRING_RTSP_SESSION_ID` only appear in the manual setter, getter, RTSP response parse/store path, and RTSP request auto-attach path. There is no pretransfer, host-change, or per-request reset path that clears the field when the RTSP URL changes to a different host or port. ## Impact ## Summary: An attacker-controlled RTSP server contacted later on the same easy handle can receive a `Session:` token that was learned from a different RTSP server. In the attached PoC, that leaked token is replayable: a fresh handle can send `PLAY` back to the victim with the leaked session ID and the victim accepts it, while the same request without the session is rejected with `454 Session Not Found`. This turns the issue from a cross-host RTSP session leak into replayable session control mix-up on reused easy handles.
Actions
View on HackerOne
Report Stats
  • Report ID: 3680234
  • State: Closed
  • Substate: informative
  • Upvotes: 1
Share this report