libcurl reuses a learned RTSP Session header across different hosts on the same easy handle, enabling cross-host session leak and replay
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 HackerOneReport Stats
- Report ID: 3680234
- State: Closed
- Substate: informative
- Upvotes: 1