curl · Report #3963494
RTSP CRLF injection in libcurl allows CURLOPT_RTSP_* values to inject commands into independent sessions
How it was found
Summary
libcurl's RTSP code interpolates the values of CURLOPT_RTSP_STREAM_URI, CURLOPT_RTSP_TRANSPORT, and CURLOPT_RTSP_SESSION_ID directly into the RTSP request text with no check for embedded CR/LF, so a value containing \r\n splits one intended RTSP operation into multiple independent RTSP requests on the wire. I reproduced this on libcurl 8.21.0 and current HEAD (5c61e16869 / 8.22.0-DEV). Using a stateful test server modeling independent named RTSP resources (the way small/embedded RTSP servers, e.g. IP cameras and NVRs, commonly track a fixed set of channels), I demonstrated that an application performing a single, legitimate SETUP on its own resource can have a smuggled TEARDOWN command injected via CURLOPT_RTSP_STREAM_URI that terminates a completely independent, already-active session established over a separate connection by an unrelated party. The victim's later request against its own session then fails with 454 Session Not Found, even though the injecting application never referenced that session or resource. None of the three affected options are documented as raw/unsanitized protocol syntax -- unlike CURLOPT_HTTPHEADER, which explicitly warns it applies no filtering -- each is documented as a single well-formed value (a stream URI, a transport spec, a session ID). AI assistance (Claude) was used during source analysis, experiment design, and drafting this report; all technical claims and the reproduction below were independently run and verified by the researcher.
Affected Version
- curl/libcurl 8.21.0 (release tag
curl-8_21_0) - Also reproduced on current HEAD, commit
5c61e16869(8.22.0-DEV) - Build used for reproduction: local build from source, OpenSSL backend
- RTSP is compiled in by default (
CURL_DISABLE_RTSPdefaults toOFF; RTSP is not listed as experimental indocs/EXPERIMENTAL.md) curl -Voutput from the two builds used:curl 8.21.0-DEV (Linux) libcurl/8.21.0-DEV OpenSSL/3.5.5 zlib/1.3.1curl 8.22.0-DEV (Linux) libcurl/8.22.0-DEV OpenSSL/3.5.5 zlib/1.3.1
Steps to Reproduce
- Build curl from the
curl-8_21_0tag (or use HEAD) with RTSP support (default-on). - Stand up a stateful RTSP test server that tracks session state per named resource path (e.g.
/camera-A,/camera-B) globally, independent of which TCP connection issues a request -- modeling how small/embedded RTSP servers commonly implement a fixed set of channels. - Victim, connection #1: send
SETUP /camera-A RTSP/1.0thenPLAY /camera-A RTSP/1.0. Server marks/camera-Aactive and returns200 OKfor both. - Unrelated application, connection #2: using an unmodified libcurl client, call
curl_easy_setopt(CURLOPT_URL, "rtsp://HOST/camera-B")and setCURLOPT_RTSP_STREAM_URIto a value containing an embedded CR/LF sequence, then issue aSETUPrequest. The application's intent is exactly one operation, on its own resource,/camera-B. - Capture the raw bytes leaving the client on connection #2 and confirm they contain two independent, well-formed RTSP requests: the intended
SETUP /camera-B RTSP/1.0followed by an attacker-influencedTEARDOWN /camera-A RTSP/1.0. - Confirm server-side that
/camera-A's session state transitions from active to torn down as a direct result of processing connection #2's bytes -- a resource that connection, that application call, and thatcurl_easy_setoptinvocation never referenced. - Victim, connection #3 (a fresh connection resuming its own session, matching the documented use of
CURLOPT_RTSP_SESSION_ID: "Useful for resuming an in-progress session"): sendPLAY /camera-A RTSP/1.0. Confirm the response is454 Session Not Foundinstead of the200 OKit would have received before step 4-6. - Repeat with a clean, well-formed
CURLOPT_RTSP_STREAM_URI/CURLOPT_RTSP_TRANSPORTvalue (control case) and confirm exactly one request is sent and no cross-session effect occurs.
This was implemented as: a standalone libcurl C reproducer (rtsp_repro.c, parameterized by mode) driving the malicious SETUP; a stateful Python RTSP test server (rtsp_stateful_server.py) tracking per-resource session state and logging every parsed request plus the resulting state table; and a driver (rtsp_crosssession_test.py) that runs the full three-connection sequence and prints the server-side causal log end to end. All three are attached to this report as the PoC.
Vulnerable Source
lib/rtsp.c:
rtsp_setup_request():b->session_id,b->stream_uri, and the value passed tortsp_header_alloc()for theTransport:header are read directly fromCURL_EASY_STR(data, STRING_RTSP_SESSION_ID / STREAM_URI / TRANSPORT)with no validation.rtsp_header_alloc():*target = curl_maprintf("%s: %s\r\n", header_name, value)-- raw, unescaped interpolation.- Request-line construction inside
rtsp_do():
curlx_dyn_addf(&req_buffer,
"%s %s RTSP/1.0\r\n" /* Request Stream-URI RTSP/1.0 */
"CSeq: %u\r\n",
block.request, block.stream_uri, rtsp->CSeq_sent);
...
curlx_dyn_addf(&req_buffer, "Session: %s\r\n", block.session_id);
block.stream_uri sits directly in the request line; block.session_id and the Transport value sit directly in header lines. None of the three is checked for CR/LF anywhere in this file.
The same function does explicitly guard against the application setting CSeq or Session as a custom header via CURLOPT_HTTPHEADER ("Session ID cannot be set as a custom header") -- the maintainers were alert to header confusion through that specific path, just not to CR/LF embedded inside the values of these three dedicated RTSP options. This pattern -- an app-supplied CURLOPT_* string interpolated unchecked into a line-oriented protocol command -- is the same shape as several fixes curl has shipped in the last two months for other protocols (SMTP envelope address, FTP ACCT, MIME part name/filename, gopher selector, MQTT topic, LDAP filter values), none of which touched RTSP.
We checked the documentation for all three options before reporting: CURLOPT_RTSP_STREAM_URI.md, CURLOPT_RTSP_TRANSPORT.md, and CURLOPT_RTSP_SESSION_ID.md each describe a single, well-formed data value (a URI, a transport specification, a session identifier) and give example values that are never raw multi-line protocol text. None carries any disclaimer comparable to CURLOPT_HTTPHEADER's explicit "libcurl passes on the verbatim strings you give it, without any filter or other safe guards. That includes white space and control characters." CURLOPT_RTSP_SESSION_ID.md even contains a security-motivated WARNING about a different risk (session-ID reuse across hosts on a reused handle) without ever mentioning CR/LF -- indicating this is a gap in what the maintainers considered, not a deliberately accepted and documented risk.
Impact
Confirmed impact
Using a stateful test server modeling independent named RTSP resources (not a real IP camera or NVR product -- a synthetic model of that class of server's session-tracking behavior), we demonstrated that a single CURLOPT_RTSP_STREAM_URI value, supplied by an application performing a SETUP against its own resource, can smuggle a second, fully independent, well-formed RTSP request (TEARDOWN) that terminates a different, already-active session established over an entirely separate connection by an unrelated party. The causal chain was traced end to end: the victim's session shows as active before the injection, the server's session-state log shows the transition to torn down attributed to the unrelated connection's request, and the victim's own subsequent request against the same resource independently confirms the loss (454 Session Not Found). This is unauthorized cross-session/cross-resource command injection with a demonstrated availability impact on a party the injecting application had no relationship to, reproduced identically on 8.21.0 and HEAD.
Limitations
- The test server is a minimal synthetic model built to isolate and prove the session-tracking-by-resource behavior pattern; we have not tested against, and are not claiming to have compromised, any specific real IP camera, NVR, or other RTSP server product. Real servers' session-identification strategies vary (some may key strictly on server-issued session tokens rather than resource path), which would change exploitability details without changing the underlying request-splitting primitive.
- Exploitation requires a necessary precondition: the application must pass attacker-controlled or attacker-influenced data into one of
CURLOPT_RTSP_STREAM_URI,CURLOPT_RTSP_TRANSPORT, orCURLOPT_RTSP_SESSION_IDwithout checking it for CR/LF first. This is not exploitable against curl by default or without that specific application behavior. - We did not attempt to demonstrate RTP/media-stream redirection via an injected
SETUPwith altered Transport parameters. That remains a plausible further consequence we have not built or verified, and we are not claiming it. - Not demonstrated: confidentiality impact, authentication bypass, or code execution.
- We have not identified a specific named open-source application using this exact option-forwarding pattern.
Weakness
Improper Neutralization of CRLF Sequences ('CRLF Injection') -- CWE-93. This is the correct protocol-agnostic classification for the mechanism (curl's own recent fixes for the same shape of bug in SMTP/FTP/MIME/gopher/MQTT/LDAP fall under the same weakness). The demonstrated consequence here is unauthorized command injection affecting an independent session/resource; if the program's taxonomy has a more specific category it considers a better fit for that consequence, we'd defer to their judgment, but CWE-93 is our primary recommendation as it accurately names the root cause.
Severity
Medium, offered as our assessment -- we recognize curl's security team makes the final determination and the illustrative CVSS vector below is not a substitute for that.
Justification: this is a demonstrated, reproducible instance of one application's single, legitimate operation on its own resource causing unauthorized termination of an independent, unrelated party's active session on a different resource -- a genuine violation of the isolation that's supposed to exist between independent RTSP sessions, not merely malformed output. We are not rating it higher than Medium because of the necessary precondition identified above (the application must first forward attacker-controlled or attacker-influenced data into one of these three options; this is not triggerable against arbitrary curl usage or by default), because the demonstration is against a synthetic test server modeling this class of server's session-tracking behavior rather than a verified real product, and because no confidentiality impact or authentication bypass has been shown.
Illustrative CVSS 3.1 vector for the demonstrated scenario: AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:L. We offer this only as a concrete reference point for how we reasoned about the demonstrated test, not as a claim of the final score.
Remediation
Add the same CR/LF rejection already used for SMTP/FTP/MIME/gopher/MQTT/LDAP at the three read points in rtsp_setup_request():
b->session_id = CURL_EASY_STR(data, STRING_RTSP_SESSION_ID);
if(b->session_id && strpbrk(b->session_id, "\r\n")) {
failf(data, "Refusing to send a Session ID with a CR or LF");
return CURLE_BAD_FUNCTION_ARGUMENT;
}
...mirrored for STRING_RTSP_STREAM_URI and the value passed into rtsp_header_alloc() for STRING_RTSP_TRANSPORT. Given how many call sites across the codebase now carry this same hand-written check, a shared helper (e.g. Curl_str_has_crlf()) used by all of them, including this one, would reduce the risk of the next protocol handler missing it.
Regression Test
Add a libcurl test case: set CURLOPT_RTSP_STREAM_URI (and separately CURLOPT_RTSP_TRANSPORT, CURLOPT_RTSP_SESSION_ID) to a value containing "\r\nX-Injected: yes", issue a SETUP/PLAY, and assert curl_easy_perform() returns CURLE_BAD_FUNCTION_ARGUMENT before anything reaches the wire -- mirroring the structure of the existing SMTP CR/LF regression test (test 2110).
AI Disclosure
AI assistance (Claude) was used during source analysis, experiment design, and preparation of this report. All technical claims, the reproduction steps, and the source citations above were independently run and verified by the researcher before submission; nothing in this report is asserted on the basis of AI output alone.
Impact
Confirmed impact
Using a stateful test server modeling independent named RTSP resources (not a real IP camera or NVR product -- a synthetic model of that class of server's session-tracking behavior), we demonstrated that a single CURLOPT_RTSP_STREAM_URI value, supplied by an application performing a SETUP against its own resource, can smuggle a second, fully independent, well-formed RTSP request (TEARDOWN) that terminates a different, already-active session established over an entirely separate connection by an unrelated party. The causal chain was traced end to end: the victim's session shows as active before the injection, the server's session-state log shows the transition to torn down attributed to the unrelated connection's request, and the victim's own subsequent request against the same resource independently confirms the loss (454 Session Not Found). This is unauthorized cross-session/cross-resource command injection with a demonstrated availability impact on a party the injecting application had no relationship to, reproduced identically on 8.21.0 and HEAD.
Limitations
- The test server is a minimal synthetic model built to isolate and prove the session-tracking-by-resource behavior pattern; we have not tested against, and are not claiming to have compromised, any specific real IP camera, NVR, or other RTSP server product.
- Exploitation requires a necessary precondition: the application must pass attacker-controlled or attacker-influenced data into one of CURLOPT_RTSP_STREAM_URI, CURLOPT_RTSP_TRANSPORT, or CURLOPT_RTSP_SESSION_ID without checking it for CR/LF first. This is not exploitable against curl by default or without that specific application behavior.
- We did not attempt to demonstrate RTP/media-stream redirection via an injected SETUP with altered Transport parameters, and are not claiming it.
- Not demonstrated: confidentiality impact, authentication bypass, or code execution.