curl · Report #4113412
libcurl RTSP `CURLOPT_CRLF` conversion allows a request body to become a second RTSP request
How it was found
AI WAS USED IN THE PROCESS OF IDENTIFYING THIS VULNERABILITY.
The vulnerability was tested manually and the report was carefully read.
Suggested severity
Low
Affected version
curl 8.23.0, tag curl-8_23_0, commit
3163ec065643ba20709f190111f42c3e1fe95273.
Affected interface
The issue is reachable through the libcurl API. It is not directly reachable
through the curl command-line frontend because the tool does not expose
CURLOPT_RTSP_REQUEST. In particular, -X ANNOUNCE sets
CURLOPT_CUSTOMREQUEST; it does not select CURL_RTSPREQ_ANNOUNCE.
Summary
For RTSP ANNOUNCE, SET_PARAMETER, and GET_PARAMETER uploads, libcurl sends a
Content-Length based on the raw input size. If CURLOPT_CRLF is enabled,
libcurl then expands bare LF bytes while transmitting the body but does not
update or reject the now-unknown length.
The server reads only the declared body length. The remaining bytes can form a second RTSP request on the same connection.
This appears to be the RTSP sibling of CVE-2026-87864. Its fix changed IMAP
APPEND to consult Curl_creader_total_length() after the reader stack was
created and reject CRLF conversion when the final size is unknown. The RTSP
path still uses its pre-conversion req_clen.
Root cause
In lib/rtsp.c, rtsp_setup_body() does the following:
req_clen = data->state.infilesize;
result = Curl_creader_set_fread(data, req_clen);
...
curlx_dyn_addf(reqp, "Content-Length: %" FMT_OFF_T "\r\n", req_clen);
Curl_creader_set_fread() installs the generic CRLF conversion reader when
data->set.crlf is true. That reader's cr_lc_total_length() correctly
returns -1, but RTSP does not query it after reader construction.
Reproduction
I used the attached rtsp-crlf-framing.c with a one-shot loopback RTSP
listener. The client constructs a body containing K bare LF bytes followed
by a K-byte RTSP OPTIONS request. CRLF conversion expands the prefix from
K to 2K bytes. The advertised raw body length is also 2K, so the body
boundary lands exactly before the complete injected request.
Build the client against curl 8.23.0:
cc -O2 -DCURL_STATICLIB -I/path/to/curl/include \
rtsp-crlf-framing.c /path/to/libcurl.a -o rtsp-crlf-framing \
[the normal static libcurl dependency flags]
In terminal 1:
python3 rtsp_capture_server.py
In terminal 2:
./rtsp-crlf-framing rtsp://127.0.0.1:21031/test
Observed client result:
raw_body=126 injected=63 result=0 (No error)
Observed by the server:
Content-Length: 126
advertised=126
actual_body=189
overflow=63
overflow_bytes=b'OPTIONS rtsp://127.0.0.1:21031/injected RTSP/1.0\r\nCSeq: 999\r\n\r\n'
The overflow is an intact second request, and curl reports success.
Public disclosure
I have not opened a public issue or otherwise disclosed this report.
Impact
Impact
If an application enables CURLOPT_CRLF and sends attacker-influenced RTSP
body content, the attacker can escape the declared body and append RTSP
methods on the application's persistent connection. Depending on the server's
connection, authentication, and session behavior, this can allow actions under
the application's RTSP context.
This is not ordinary user-selected CRLF header injection: these bytes are provided as framed body content, and curl's conversion moves them outside that boundary. That is the same distinction as CVE-2026-87864.
The prerequisites are uncommon, so I suggest Low severity.
Suggested fix
After installing the RTSP body reader, use Curl_creader_total_length(data)
for Content-Length. If it returns a negative value, reject the upload with
CURLE_UPLOAD_FAILED, as RTSP cannot switch these requests to chunked framing.
A regression test can extend RTSP test568 with a known-size ANNOUNCE body that
contains bare LF bytes and enables CURLOPT_CRLF. It should assert
CURLE_UPLOAD_FAILED and no transmitted overflow.