CRLF injection in WebSocket upgrade request in hackney
Description
Improper Neutralization of CRLF Sequences ('CRLF Injection') vulnerability in benoitc hackney allows HTTP Request/Response Splitting. The WebSocket upgrade code in src/hackney_ws.erl copies the host, path, headers (ExtraHeaders), and protocols options from the caller-supplied opts map into the internal #ws_data{} record in init/1 and then splices them verbatim into the raw HTTP/1.1 upgrade request by binary concatenation in do_handshake/1. No CRLF or NUL stripping is performed at any of these four injection sites. An attacker who controls any of these options — for example by forwarding URL components or header values from untrusted input into hackney_ws:start_link/1 — can inject arbitrary HTTP headers into the outbound WebSocket upgrade request, leading to header injection, credential spoofing toward the upstream server, log and cache poisoning, or request smuggling via intermediary proxies.
This issue affects hackney: from 2.0.0 before 4.0.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CRLF injection in hackney's WebSocket upgrade request allows header injection and request smuggling via unsanitized host, path, or header options.
Vulnerability
A CRLF injection vulnerability exists in the do_handshake/1 function in src/hackney_ws.erl before version 4.0.1 [1][3][4]. The init/1 function copies the host, path, ExtraHeaders, and protocols options from the caller-supplied opts map verbatim into the internal #ws_data{} record, and do_handshake/1 splices these fields directly into the raw HTTP/1.1 upgrade request using binary concatenation without any CRLF or NUL character stripping [1][4]. This affects hackney versions from 2.0.0 up to but not including 4.0.1 [1][2].
Exploitation
An attacker who can control any of the four options (host, path, ExtraHeaders, or protocols) passed to hackney_ws:start_link/1 can inject arbitrary HTTP headers into the outbound WebSocket upgrade request [1][4]. For example, by providing a header value containing \r\n followed by additional header lines, the attacker can inject standalone headers such as Authorization: Bearer attacker [4]. The attack requires no authentication or user interaction, only the ability to influence the options passed to the vulnerable function (e.g., by forwarding URL components or header values from untrusted input) [1][2].
Impact
Successful exploitation allows HTTP request splitting and header injection, leading to credential spoofing toward the upstream server, log and cache poisoning, or HTTP request smuggling via intermediary proxies [1][2]. The attacker can forge authentication headers, poison web caches, or manipulate the upgrade request to interfere with the target server's interpretation of subsequent requests [4]. The CVSS v4.0 score is 6.9 (MEDIUM) [1][2].
Mitigation
The fix is available in hackney version 4.0.1 [1][2]. The patch (commit 52310ca) adds a valid_handshake_fields/2 function that rejects fields containing bytes \r, \n, or \0 before the handshake request is built and sent [3]. Users should upgrade to version 4.0.1 or later. If upgrading is not immediately possible, all four options (host, path, ExtraHeaders, protocols) must be sanitized by the caller to strip or reject CR/LF/NUL characters before being passed to hackney_ws:start_link/1 [4].
AI Insight generated on May 25, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
152310ca807e7fix(security): reject CR/LF/NUL in WebSocket upgrade request (GHSA-f9vr)
2 files changed · +52 −10
src/hackney_ws.erl+36 −9 modified@@ -565,7 +565,7 @@ do_connect_via_socks5(Transport, Host, Port, ProxyHost, ProxyPort, end. %% @private Perform WebSocket handshake -do_handshake(#ws_data{socket = Socket, transport = Transport, host = Host, +do_handshake(#ws_data{transport = Transport, host = Host, port = Port, path = Path, headers = ExtraHeaders, protocols = Protocols} = Data) -> %% Generate random key @@ -605,15 +605,42 @@ do_handshake(#ws_data{socket = Socket, transport = Transport, host = Host, %% Add extra headers Headers2 = Headers1 ++ ExtraHeaders, - %% Build request - HeaderLines = [[Name, <<": ">>, Value, <<"\r\n">>] || {Name, Value} <- Headers2], - Request = [ - <<"GET ">>, Path, <<" HTTP/1.1\r\n">>, - HeaderLines, - <<"\r\n">> - ], + %% GHSA-f9vr: the upgrade request is assembled by raw concatenation, so + %% a caller-supplied host, path, sub-protocol or extra header carrying + %% CR/LF/NUL would splice in extra header lines or rewrite the request + %% line. Reject those bytes before anything reaches the socket. + case valid_handshake_fields(Path, Headers2) of + ok -> + %% Build request + HeaderLines = [[Name, <<": ">>, Value, <<"\r\n">>] || {Name, Value} <- Headers2], + Request = [ + <<"GET ">>, Path, <<" HTTP/1.1\r\n">>, + HeaderLines, + <<"\r\n">> + ], + do_send_handshake(Data, Key, Request); + {error, _} = Err -> + Err + end. + +%% @private GHSA-f9vr: reject CR/LF/NUL in the request line path and in any +%% header name/value used to build the WebSocket upgrade request. +valid_handshake_fields(Path, Headers) -> + Fields = [Path | lists:flatmap(fun({N, V}) -> [N, V] end, Headers)], + case lists:any(fun has_ctl_bytes/1, Fields) of + true -> {error, invalid_handshake_header}; + false -> ok + end. + +has_ctl_bytes(Bin) when is_binary(Bin) -> + binary:match(Bin, [<<"\r">>, <<"\n">>, <<0>>]) =/= nomatch; +has_ctl_bytes(L) when is_list(L) -> + has_ctl_bytes(iolist_to_binary(L)); +has_ctl_bytes(_) -> + false. - %% Send request +%% @private Send the assembled upgrade request and process the response. +do_send_handshake(#ws_data{socket = Socket, transport = Transport} = Data, Key, Request) -> case Transport:send(Socket, Request) of ok -> %% Read response
test/hackney_ws_tests.erl+16 −1 modified@@ -92,10 +92,12 @@ ws_integration_test_() -> {setup, fun start_ws_server/0, fun stop_ws_server/1, - fun({_ListenerName, _Port, WsUrl}) -> + fun({_ListenerName, Port, WsUrl}) -> [ {"Connect and disconnect", fun() -> test_connect_disconnect(WsUrl) end}, + {"GHSA-f9vr: CRLF in upgrade header rejected", + fun() -> test_handshake_header_injection(Port) end}, {"Send and receive text message", fun() -> test_text_message(WsUrl) end}, {"Send and receive binary message", @@ -115,6 +117,19 @@ ws_integration_test_() -> ] end}. +%% GHSA-f9vr: a CRLF-bearing extra header must abort the upgrade before any +%% bytes are written, not splice an injected header onto the wire. +test_handshake_header_injection(Port) -> + {ok, Ws} = hackney_ws:start_link(#{ + host => "localhost", + port => Port, + transport => hackney_tcp, + path => <<"/ws">>, + headers => [{<<"X-Evil">>, <<"v\r\nX-Injected: yes">>}] + }), + ?assertEqual({error, invalid_handshake_header}, + hackney_ws:connect(Ws, 5000)). + test_connect_disconnect(WsUrl) -> {ok, Ws} = hackney:ws_connect(WsUrl), ?assert(is_pid(Ws)),
Vulnerability mechanics
Root cause
"Missing CRLF and NUL sanitization in the WebSocket upgrade request builder allows header injection via caller-supplied host, path, headers, or protocols fields."
Attack vector
An attacker who controls any of the `host`, `path`, `headers`, or `protocols` options passed to `hackney_ws:start_link/1` — for example by forwarding URL components or header values from untrusted input — can inject arbitrary HTTP headers into the outbound WebSocket upgrade request [ref_id=2]. The `do_handshake/1` function builds the request by raw concatenation at four sites: the Host header line, the Sec-WebSocket-Protocol header line, each extra header name/value pair, and the request line itself [ref_id=2]. A header value containing `\r\n` produces multiple header lines on the wire, enabling header injection, credential spoofing toward the upstream server, log and cache poisoning, or request smuggling via intermediary proxies [CWE-93][ref_id=2].
Affected code
The vulnerability resides in `src/hackney_ws.erl`, specifically in the `do_handshake/1` function. The `host`, `path`, `headers` (ExtraHeaders), and `protocols` options from the caller-supplied `opts` map are copied verbatim into the `#ws_data{}` record in `init/1` and then spliced directly into the raw HTTP/1.1 upgrade request by binary concatenation with no CRLF or NUL stripping [ref_id=1][ref_id=2].
What the fix does
The patch introduces a `valid_handshake_fields/2` guard that checks the path and every header name/value for CR (`\r`), LF (`\n`), and NUL (`\0`) bytes before the request is assembled [patch_id=2473697]. If any field contains these control bytes, the handshake aborts with `{error, invalid_handshake_header}` and no bytes are sent to the socket [ref_id=1]. The `do_handshake/1` function was refactored to split the validation from the send logic, moving the actual send into a new `do_send_handshake/3` function that is only reached after validation passes [patch_id=2473697].
Preconditions
- inputThe attacker must be able to supply or influence the host, path, headers, or protocols options passed to hackney_ws:start_link/1, typically by forwarding untrusted user input into these fields.
- configThe application must use hackney's WebSocket upgrade functionality (hackney_ws) with version 2.0.0 through 4.0.0.
Reproduction
Call `hackney_ws:start_link/1` with a headers list containing a CRLF sequence, e.g. `headers => [{«X-User», «v\r\nAuthorization: Bearer attacker»}]`. Connect to a raw TCP listener and observe that the outbound upgrade request contains an injected `Authorization: Bearer attacker` header line [ref_id=2]. The test added in the patch (`test_handshake_header_injection`) demonstrates that such input now returns `{error, invalid_handshake_header}` instead of being sent [patch_id=2473697].
Generated on May 25, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/benoitc/hackney/commit/52310ca807e7b48441ba0e9129171f535313fdd1mitrepatch
- github.com/benoitc/hackney/security/advisories/GHSA-f9vr-g2g2-x9fgmitrevendor-advisoryrelated
- cna.erlef.org/cves/CVE-2026-47072.htmlmitrerelated
- osv.dev/vulnerability/EEF-CVE-2026-47072mitrerelated
News mentions
0No linked articles in our index yet.