HexBrain

curl · Report #3963494

RTSP CRLF injection in libcurl allows CURLOPT_RTSP_* values to inject commands into independent sessions

N/Astatus
mediumseverity
17upvotes
24 Aug 2026disclosed

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_RTSP defaults to OFF; RTSP is not listed as experimental in docs/EXPERIMENTAL.md)
  • curl -V output from the two builds used:
    • curl 8.21.0-DEV (Linux) libcurl/8.21.0-DEV OpenSSL/3.5.5 zlib/1.3.1
    • curl 8.22.0-DEV (Linux) libcurl/8.22.0-DEV OpenSSL/3.5.5 zlib/1.3.1

Steps to Reproduce

  1. Build curl from the curl-8_21_0 tag (or use HEAD) with RTSP support (default-on).
  2. 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.
  3. Victim, connection #1: send SETUP /camera-A RTSP/1.0 then PLAY /camera-A RTSP/1.0. Server marks /camera-A active and returns 200 OK for both.
  4. Unrelated application, connection #2: using an unmodified libcurl client, call curl_easy_setopt(CURLOPT_URL, "rtsp://HOST/camera-B") and set CURLOPT_RTSP_STREAM_URI to a value containing an embedded CR/LF sequence, then issue a SETUP request. The application's intent is exactly one operation, on its own resource, /camera-B.
  5. 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.0 followed by an attacker-influenced TEARDOWN /camera-A RTSP/1.0.
  6. 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 that curl_easy_setopt invocation never referenced.
  7. 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"): send PLAY /camera-A RTSP/1.0. Confirm the response is 454 Session Not Found instead of the 200 OK it would have received before step 4-6.
  8. Repeat with a clean, well-formed CURLOPT_RTSP_STREAM_URI/CURLOPT_RTSP_TRANSPORT value (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 to rtsp_header_alloc() for the Transport: header are read directly from CURL_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, 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. 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.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original