curl · Report #4121689
CRLF injection in the HTTP request-line via `CURLOPT_REQUEST_TARGET` (`--request-target`)
How it was found
Summary
The CURLOPT_REQUEST_TARGET option string reaches the wire with no CR/LF neutralization, even though it is formatted directly into the HTTP request-line, which is terminated by CRLF. Curl_setstropt() (lib/setopt.c) stores the option string after checking only the 8 MB length cap (CURL_MAX_INPUT_LENGTH) — it never inspects for \r or \n — and the request-line construction in http_target() (lib/http.c) then splices the stored value verbatim into the request buffer with curlx_dyn_add, which does not filter control characters. An embedded \r\n therefore terminates the request-line early and lets the caller write arbitrary additional request-lines or headers into the same TCP stream, enabling header injection and HTTP request smuggling.
A related CRLF-injection issue in the RTSP option strings (CURLOPT_RTSP_STREAM_URI / CURLOPT_RTSP_TRANSPORT, lib/rtsp.c) is reported separately.
Affected versions
- curl 8.21.0-DEV (debug build, mbedTLS 3.6.5) — reproduced.
- curl master
9b2caf3(8.22.0-DEV, verified 2026-08-31) — still affected; re-run against a master build, reproduces byte-for-byte. - Source-verified still present on current master as of 2026-10-16: no CRLF filtering has been added to
Curl_setstropt()orhttp_target(), and neither the 8.22.0 nor the 8.23.0 changelog contains a relevant fix. - Affects every curl version since
CURLOPT_REQUEST_TARGETwas introduced (7.55.0).
Root cause
Curl_setstropt() is the store for the option. It only enforces the length cap and never looks at byte content:
// lib/setopt.c:88-96 (Curl_setstropt)
if(strlen(s) > CURL_MAX_INPUT_LENGTH)
/* …too long… */;
else
dest = curlx_strdup(s); // no CR/LF (or control-char) check; CRLF preserved verbatim
The value is then written into the CRLF-delimited request-line with no additional filtering:
// lib/http.c:2086-2088 (http_target)
if(data->set.str[STRING_TARGET]) {
path = data->set.str[STRING_TARGET]; // user value, CRLF intact
query = NULL;
}
// lib/http.c:2173 (non-proxy path)
curlx_dyn_add(r, path); // raw append into the request-line buffer
// lib/http.c:2881-2888 (http_add_hd, H1_HD_REQUEST branch)
curlx_dyn_addf(req, "%s ", method); // "GET "
http_target(data, req); // appends STRING_TARGET verbatim, incl. CRLF
curlx_dyn_addf(req, " HTTP/%s\r\n", …); // trailing version + line terminator
Because the injected CRLF lands inside the request-line, the attacker controls framing, not just a single header field: a single \r\n ends the current line and starts a new header, and a \r\n\r\n ends the current request's header section and starts a new request.
PoC
A mock TCP server is run on loopback to capture the raw bytes curl writes. The variant is driven the way a real application would drive it — via the --request-target CLI flag (which maps to CURLOPT_REQUEST_TARGET):
--request-target "/legitimate\r\nX-Injected: evil-header\r\n\r\nGET /admin HTTP/1.1"(the\r\nare literal CR/LF bytes in the argument).- curl accepts the value without rejection and sends the request.
- Negative control:
--request-target "/legitimate"produces a single clean request-line.
The variant was re-run against a curl master 9b2caf3 build; the behavior is identical to 8.21.0-DEV.
Reproduction output
Raw bytes captured by the mock HTTP server (hex, --request-target payload):
GET /legitimate\r\n
X-Injected: evil-header\r\n
\r\n
GET /admin HTTP/1.1 HTTP/1.1\r\n
Host: 127.0.0.1:19997\r\n
User-Agent: curl/8.21.0-DEV\r\n
Accept: */*\r\n
\r\n
Hex dump confirms the CRLF after /legitimate (0d 0a at offset 0x0e–0x0f), followed by the injected X-Injected: evil-header line and the smuggled GET /admin HTTP/1.1 request-line. Negative control: a clean target produces only GET /legitimate HTTP/1.1\r\n with no injection.
Impact
- CVSS 3.1: 7.5 (High) —
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N, scored on the most reliable vector (header injection → authentication bypass → read of protected data on the target server, achievable in a single request with no front-end/back-end architecture required). The request-smuggling path additionally raises integrity and availability impact (injectedPOST/DELETEreaching the backend) under a front-proxy/back-server topology; that path requires a proxy that enforces request-path access control and a backend that disagrees about request boundaries, and the smuggled request's response is not returned to the caller. - Trigger conditions: an application forwards attacker-controlled data into
CURLOPT_REQUEST_TARGETwithout filtering CR/LF first. Realistic settings include REST/API client libraries that let a caller specify the request path. The curl command-line's own--request-targetis controlled by the local user and is not by itself a remote attack surface; the remote exposure runs through applications that surface this option to remote input. - Realized impact (verified end-to-end): injected
Cookie/Authorizationheaders are honored by a target server that authenticates by those headers, granting access to protected resources; a complete smuggled second request (GET /admin/secret) bypasses a front proxy's path-based access control and reaches the backend. - Severity bound: exploitation is gated on an application forwarding user input into this setopt option without CRLF filtering, and on the target server / proxy honoring injected headers or disagreeing on request boundaries. The curl documentation does note that
CURLOPT_REQUEST_TARGETis passed "verbatim … without any filter … including white space and control characters," but the libcurl security page's "CRLF in Headers" section does not listCURLOPT_REQUEST_TARGET, so application authors are not signposted toward filtering.