Summary
The WebSocket client in src/hackneyws.erl imposes no upper bound on memory consumption across three distinct code paths. In each case, an attacker-controlled WebSocket server can exhaust the connecting process's memory without any authentication or special client configuration.
Details
1. Handshake response buffer (readhandshakeresponse/3)
The function accumulates received bytes into a growing buffer waiting for \r\n\r\n. The per-receive timeout resets on every chunk, so a server that trickles bytes indefinitely without completing the HTTP upgrade response grows the buffer until OOM. No total-size cap exists.
2. Frame payload accumulation (parsepayload/9, parseactivepayload/8)
parsepayload/9 (lines 816–817 and 825–826) appends each received chunk into a Buffer binary via <<Buffer/binary, MoreData/binary>> whenever the frame parser returns {more, ...}. parseactivepayload/8 does the same in active mode by appending each incoming tcp/ssl message to #wsdata.buffer. RFC 6455 permits payload lengths up to 2⁶³-1 bytes, and neither path validates the declared Len against any limit. The recvtimeout applies per chunk, not to the whole frame, so a slow trickle never triggers it.
3. Fragmentation buffer (fragbuffer)
The fragbuffer field of #wsdata{} accumulates continuation frames. A server that sends an unbounded stream of non-final (nofin) fragments without ever sending a final (fin) frame grows fragbuffer without bound.
PoC
1. Stand up a WebSocket server and connect to it with hackney's WebSocket client. 2. Trigger any of the three paths: (a) never send \r\n\r\n during the handshake; (b) announce a very large frame payload and dribble bytes slowly; (c) send an endless stream of nofin continuation frames. 3. Observe the hackney process's memory growing until the BEAM OOM-kills it or the node crashes.
Impact
Denial of service via unbounded memory consumption. Affects hackney 2.0.0 through 4.0.0 for any application using the WebSocket client against an attacker-controlled server. No authentication or special configuration is required on the client side. CVSS v4.0: 8.7 (HIGH).
Resources
Introduction commit: https://github.com/benoitc/hackney/commit/690cecaf236fba49526da404a5bc889a24367a3e Patch commit: https://github.com/benoitc/hackney/commit/ce0109e2970ace6e20ff29bae9d05c3ac22ec6dc
Summary
CVE-2026-47067 is an atom table exhaustion vulnerability (CWE-770) in hackney's URL parser (src/hackneyurl.erl). hackneyurl:parseurl/1 converts every URL scheme it encounters into a BEAM atom via binarytoatom/2. Because BEAM atoms are never garbage-collected and the atom table has a hard limit of 1,048,576 entries, an attacker who can feed URLs with attacker-chosen scheme prefixes — directly as request targets, as webhook/callback URLs, or via Location headers in redirect chains — can exhaust the atom table and crash the entire BEAM VM with systemlimit.
Details
1. Scheme extraction and conversion
In src/hackneyurl.erl, parseurl/1 extracts the scheme binary (the part before ://), validates it with isvalidscheme/1 (RFC 3986 alphabet: alpha-led, <=19 bytes, alphanumeric/+/-/. body), lowercases it, then calls:
erlang binarytoatom(SchemeLower, utf8)
The resulting atom is stored on the #hackneyurl{} record and returned to the caller.
2. Permanent atom accumulation
The validation constrains the alphabet but not uniqueness. The allowed scheme space is enormous (≈52·65¹⁸ values), far exceeding the default atom limit of 1,048,576. Each distinct scheme mints a new permanent atom. Even when hackney subsequently rejects an unsupported scheme with {error, {unsupportedscheme, }}, the atom has already been interned and is never reclaimed.
3. Crash vector
The most dangerous path is redirect following: when hackney follows a Location header, the redirect target URL is re-parsed by the same function. An attacker-controlled server can serve a sequence of redirects — or a batch of URLs from an upstream feed — each with a fresh unique scheme, driving the atom count monotonically upward. At the limit the BEAM emits systemlimit and the node terminates; recovery requires a full restart.
PoC
1. Call hackneyurl:parseurl/1 (or :hackney.request/5) repeatedly with URLs whose scheme prefixes are unique on each call: aaaa://x, aaab://x, aaac://x, … 2. After enough iterations, observe erlang:systeminfo(:atomcount) climbing by one per unique scheme. 3. At 1,048,576 atoms the VM crashes with systemlimit.
Alternatively, point hackney at a server that replies with a feed of ~1M URLs with distinct schemes (or uses redirect chains with rotating schemes); the atom table is exhausted and the node crashes without the client being able to intervene.
Impact
Unauthenticated remote denial of service via permanent resource exhaustion leading to VM termination. Any application using hackney 2.0.0 through 4.0.0 that processes attacker-influenced URLs — direct request targets, webhook URLs, or Location headers in followed redirects — is affected. No authentication or special configuration is required. CVSS v4.0 score: 8.7 (HIGH).
References
Introduction commit: https://github.com/benoitc/hackney/commit/d9713695c0d99855d12c73fd8a0b4be0543950c4 Patch commit: https://github.com/benoitc/hackney/commit/31f6f0e27e096ad88743dfded4f030a3ee74972e
Summary
CRLF injection in hackney's WebSocket upgrade request builder (src/hackneyws.erl). init/1 copies the host, path, headers, and protocols options from the caller-supplied opts map verbatim into #wsdata{}, and dohandshake/1 splices them directly into the raw HTTP/1.1 upgrade request by binary concatenation with no \r\n or \0 stripping. A caller that passes any of these fields from untrusted input can inject arbitrary header lines into the outbound upgrade request.
Details
dohandshake/1 builds the upgrade request at several concatenation sites:
- Host header (lines 583–590): the host binary is written straight into Host: <host>:<port>\r\n. - Sec-WebSocket-Protocol (lines 601–602): protocol tokens are joined with , and appended as a header line. - Extra headers (line 606): caller-supplied {Name, Value} tuples are concatenated as Name: Value\r\n with no sanitization of either component. - Request path (line 611): the path is interpolated into the GET <path> HTTP/1.1\r\n request line.
None of these sites reject \r, \n, or \0. A header value like <<"benign\r\nAuthorization: Bearer token">> produces two distinct header lines on the wire. A path with an embedded \r\n rewrites the request line itself.
PoC
1. Call :hackneyws.startlink/1 with headers: [{"X-User", "v\r\nAuthorization: Bearer attacker"}]. 2. Connect to a raw TCP listener and capture the bytes hackney writes. 3. The request contains a standalone Authorization: Bearer attacker line that the upstream WebSocket server parses as a legitimate header.
Impact
Header injection / request smuggling in outbound WebSocket upgrades. Affects hackney 2.0.0 through 4.0.0 wherever host, path, headers, or protocols options are populated from network or user input. Consequences include forging authentication headers toward the upstream server, log and cache poisoning, and request smuggling through intermediary proxies. CVSS v4.0: 6.9 (MEDIUM).
Resources
Introduction commit: https://github.com/benoitc/hackney/commit/690cecaf236fba49526da404a5bc889a24367a3e Patch commit: https://github.com/benoitc/hackney/commit/52310ca807e7b48441ba0e9129171f535313fdd1
Summary
hackneyurl:normalize/2 URL-decodes the host component of a parsed URL, but the caller's SSRF allowlist runs before normalization using OTP's uristring:parse/1 and inet:parseaddress/1, neither of which decodes percent-escapes in hostnames. A URL like http://%31%32%37%2E%30%2E%30%2E%31/ presents an encoded, non-IP-looking host to the validator, which passes the allowlist check; hackney's normalizer then decodes it to 127.0.0.1 and connects to loopback. Because hackney:request/5 always calls normalize/2 with no opt-out, every request path that accepts a binary or list URL is affected. This is a parser-differential SSRF in the same class as CVE-2025-1211, but in a different function.
Details
In src/hackneyurl.erl (lines 161–186), normalize/2 checks whether the parsed host is already a dotted-quad or IPv6 literal via inetparse:address/1. Percent-encoded forms like %31%32%37%2E%30%2E%30%2E%31 fail that check and fall into the catch-all branch, where urldecode/1 decodes the host before passing it to IDNA conversion:
erlang Host1 = binarytolist( urldecode(unicode:characterstobinary(Host0)) ),
The decoded host ("127.0.0.1") replaces the original in the returned #hackneyurl{} record. hackney:request/5 at src/hackney.erl:463 always calls normalize/2, so the decoded host is what dodispatch/1 and addhostheader/2 ultimately use. The on-wire Host: header and the TCP connect target both reflect the decoded value.
The same payload pattern reaches the AWS/GCP/Azure IMDS (169.254.169.254), RFC1918 ranges, and any localhost admin endpoint. The 1.21.0 patch for CVE-2025-1211 fixed a separate differential in parseurl/1 and did not touch normalize/2.
PoC
1. Validate the URL with the canonical Erlang SSRF allowlist: uristring:parse/1 returns host <<"%31%32%37%2E%30%2E%30%2E%31">>, inet:parseaddress/1 returns {error, einval}, so the allowlist accepts it. 2. Pass the same URL to hackney:get/1. 3. hackney's normalize/2 decodes the host to "127.0.0.1" and connects to 127.0.0.1:80. The internal service receives the request with Host: 127.0.0.1.
Impact
Unauthenticated SSRF bypassing the canonical Erlang allowlist pattern. Affects hackney 0.13.0 through 4.0.0 for any application that accepts attacker-supplied URLs. Targets include cloud IMDS endpoints, localhost admin interfaces, and RFC1918 backends. CVSS v4.0: 6.9 (MEDIUM).
Resources
Introduction commit: https://github.com/benoitc/hackney/commit/4d725507588942fd00efca15b86da3273656510a Patch commit: https://github.com/benoitc/hackney/commit/452620a92ec1da2e6b4862a049a2a4f04b42068f
Summary
The HTTP/3 redirect handler in src/hackneyh3.erl forwards the original request headers (Authorization, Cookie, Proxy-Authorization) and, for 307/308 responses, the original request body to the redirect target without checking whether the target host matches the origin. When followredirect is enabled and a server responds with a cross-origin Location, hackney delivers the caller's credentials verbatim to the attacker-controlled host. The main hackney HTTP/1 client has maybestripauthonredirect/2 (the fix for CVE-2018-1000007); the H3 client was added later without it.
Details
In src/hackneyh3.erl, handleredirect/11 (line 165) extracts the redirect target from the server-controlled Location header via getredirectlocation/1 and resolves it with resolveredirecturl/2, which accepts any absolute http:// or https:// URL. It then calls dorequestwithredirect/8 passing the original Headers list unchanged. For 307/308 responses, redirectmethod/2 preserves the original method and body, so the POST body is also forwarded.
No comparison is made between the original URL's scheme, host, or port and the redirect target. The downstream connect/3 opens a new QUIC connection to whatever the Location header named, and buildrequestheaders/4 serializes the unmodified headers into the QPACK-encoded request.
PoC
1. Issue an HTTP/3 POST to an attacker-controlled origin with followredirect => true and an Authorization: Bearer ... header. 2. The attacker's server responds 307 Location: https://other.host/collect. 3. hackney opens a new connection to other.host and re-sends the original headers and body, including the bearer token and any Cookie headers.
Impact
Credential and request-body disclosure to attacker-controlled origins. Affects hackney 3.1.1 through 4.0.0 when using the HTTP/3 client with followredirect enabled. Any upstream that is malicious, compromised, or reachable via DNS/MITM can steal session tokens, bearer credentials, and POST bodies. CVSS v4.0: 6.0 (MEDIUM).
Resources
Introduction commit: https://github.com/benoitc/hackney/commit/e61b7d04b7826847e1efe614106ef4d580c78eab Patch commit: https://github.com/benoitc/hackney/commit/c58d5b50bade146360b85caf3dc8065807b08246
Summary
hackneyurl:makeurl/3 passes the URL query component directly into the HTTP/1.1 request target without percent-encoding \r or \n. RFC 3986 §3.4 requires characters outside the query grammar to be percent-encoded, but no validation or escaping occurs. An attacker who controls any portion of the URL passed to hackney can inject raw CRLF sequences into the request line, enabling HTTP header injection or full request splitting.
Details
hackneyurl:makeurl/3 in src/hackneyurl.erl builds the request target by concatenating the path and query binaries. The query string is used as-is — no character-class check, no percent-encoding of \r or \n. When hackney serializes the request, the query value lands verbatim in the GET <path>?<query> HTTP/1.1\r\n request line. A query value containing \r\n terminates the request line early; subsequent bytes are parsed by the server (or any intermediary proxy) as additional header lines or a second request.
A concrete example: a URL with query ?q=x HTTP/1.1\r\nX-Injected: yes\r\nX: produces the following on the wire:
GET /?q=x HTTP/1.1 X-Injected: yes X: HTTP/1.1 Host: ...
The server sees X-Injected: yes as a legitimate request header that the client never intended to send.
PoC
1. Listen on a raw TCP port: nc -lvnp 8080. 2. Issue: :hackney.get("http://127.0.0.1:8080/?q=x HTTP/1.1\r\nX-Injected: yes\r\nX:"). 3. Observe the listener receives X-Injected: yes as a standalone header line in the request.
Impact
HTTP header injection and request splitting against any server hackney connects to. Affects all released versions of hackney before 4.0.1. Exploitation requires an attacker-controlled URL (or URL component) to reach hackney without prior sanitization. Consequences include injecting arbitrary headers (Authorization, Host, X-Forwarded-For) and splitting requests through proxies. CVSS v4.0: 6.8 (MEDIUM).
Resources
Introduction commit: https://github.com/benoitc/hackney/commit/8bb1a359a81ae58567c84f8d24564e9742e6f2bd Patch commit: https://github.com/benoitc/hackney/commit/ca73dd0aba0ed557449c18288bf07241671a43c9
Summary
hackneyh3:awaitresponseloop/6 in src/hackneyh3.erl accumulates the HTTP/3 response body in memory without any size cap. The after Timeout clause is a per-message inactivity timer, not a wall-clock deadline: every received streamdata chunk, housekeeping select message, or settings frame resets it. A malicious HTTP/3 server that drips one small chunk every Timeout - 1 ms with Fin = false and never terminates the stream keeps the loop alive indefinitely while the accumulation buffer grows without bound, eventually exhausting the BEAM process heap.
Details
In src/hackneyh3.erl, awaitresponseloop/6 (line 430) builds the body with:
erlang NewBody = <<AccBody/binary, Data/binary>>
There is no maxbody check and no monotonic deadline. The after Timeout clause at line 463 is restarted on each loop iteration. A server that ensures at least one message arrives within Timeout ms indefinitely (one small chunk per interval is sufficient) prevents the timeout from firing while AccBody grows linearly. The same module's waitconnected/3 (lines 388-389) shows the correct pattern: track an absolute start time and pass a shrinking Remaining budget into each receive. This loop does not.
Configurations
Only the HTTP/3 transport is affected. Applications using the default TCP/TLS hackney transport are not vulnerable. The vulnerability requires using hackneyh3 directly or passing {transport, h3} to hackney:request/5.
PoC
1. Stand up an HTTP/3 server that responds with 200 OK headers (Fin = false), then emits a small streamdata chunk every Timeout - margin ms with Fin = false indefinitely. 2. Issue hackney:request(get, Url, [], <<>>, [{transport, h3}]) against it. 3. Watch the client process heap grow monotonically. The configured timeout never fires; the process is eventually killed by maxheapsize or the OS OOM killer.
Impact
Remote denial of service via unbounded memory consumption. Affects hackney 2.0.0 through 4.0.0 when using the HTTP/3 transport against an attacker-controlled or attacker-influenced server. Each affected request consumes unbounded memory until the BEAM is killed. CVSS v4.0: 8.2 (HIGH).
Resources
Introduction commit: https://github.com/benoitc/hackney/commit/0334af206d5099fdf510ed9eda18e34396f065ad Patch commit: https://github.com/benoitc/hackney/commit/3d25f9fea26c90609de9d64366fedfe5065413bc
Summary
The SOCKS5 transport in src/hackneysocks5.erl correctly applies the caller-supplied timeout to the SOCKS5 negotiation phase, but then upgrades the tunnel to TLS using ssl:connect/2 (the two-argument form), which defaults to infinity. The Timeout value is in scope at that call site but is never forwarded. A hostile or man-in-the-middled SOCKS5 proxy that completes the SOCKS5 handshake normally and then stalls the TLS exchange will pin the connecting Erlang process and socket indefinitely, regardless of any connecttimeout or recvtimeout options the caller set.
Details
In src/hackneysocks5.erl, line 65, after the SOCKS5 negotiation succeeds, the code calls:
erlang ssl:connect(Socket, SSLOpts)
The three-argument form ssl:connect/3 takes a timeout; the two-argument form used here defaults to infinity. The variable Timeout (already used for SOCKS5 recv calls earlier in the same function) is simply not passed. The bytes that drive the TLS handshake on the upstream side of the tunnel come from whatever endpoint the proxy connects to. A hostile proxy can complete SOCKS5 normally, then either stay silent or send a partial ServerHello and stop, keeping ssl:connect/2 blocked forever. No certificate forgery is needed.
PoC
1. Stand up a SOCKS5 proxy that completes the SOCKS5 greeting and CONNECT reply normally, then goes silent (never sends a TLS ServerHello). 2. Issue an HTTPS request through it via hackney with connecttimeout and recvtimeout set to a short value (e.g. 2000 ms). 3. Observe the calling process remains blocked well past the configured timeout, consuming a process and socket until killed externally.
Impact
Denial of service via unbounded process and socket consumption. Affects hackney 0.10.0 through 4.0.0 for any HTTPS request routed through a SOCKS5 proxy. The connecttimeout and recvtimeout options give a false sense of safety since they are not honored during the TLS upgrade. CVSS v4.0: 8.2 (HIGH).
Resources
Introduction commit: https://github.com/benoitc/hackney/commit/34cdbd1d20a282aacc286a89327465a3925b4c5d Patch commit: https://github.com/benoitc/hackney/commit/5ccdab725c561a6f03d05a51f2d0664f98236dae
Summary
CRLF injection in hackneycookie:setcookie/3 (src/hackneycookie.erl). The function validates Name and Value against CR/LF and control characters but concatenates the domain and path options verbatim into the output binary. If either option carries attacker-controlled data, a Host header forwarded as the cookie domain, a request URI forwarded as the cookie path, a \r\n in the value splits the Set-Cookie header and lets the attacker inject additional headers into the HTTP response.
Details
1. Asymmetric validation
Lines 27–34 of hackneycookie.erl run binary:match on Name and Value, rejecting =, ,, ;, whitespace, \r, \n, \013, and \014. The Domain and Path options (lines 47 and 51) skip this check entirely and land straight in the result iolist:
erlang [<<"; Domain=">>, Domain] [<<"; Path=">>, Path]
iolisttobinary(...) on line 63 flattens everything and returns it to the caller.
2. Injection
A Path of <<"/x\r\nSet-Cookie: admin=1; Path=/">> produces a binary with a literal \r\n. Written into a Set-Cookie response header, the receiving HTTP parser splits it into two headers — one legitimate, one attacker-controlled.
3. Realistic trigger
Common patterns: keying the cookie domain off Host, deriving the path from the request URI, or copying a Location path into a cookie. Any of these lets a remote attacker control the injected content.
PoC
1. Call hackneycookie:setcookie(<<"sid">>, <<"abc">>, [{path, <<"/x\r\nSet-Cookie: admin=1; Path=/">>}]). 2. The returned binary contains a literal \r\n followed by a second Set-Cookie: line. 3. Write the result into a Set-Cookie response header — the client parses two headers, including admin=1.
Impact
Cookie injection / HTTP response splitting at the hackneycookie API boundary. Affects hackney 0.9.0 through 4.0.0 wherever domain or path options are populated from request data. Exploitation can overwrite session/auth cookies, fix cookies, or strip Secure/HttpOnly flags. CVSS v4.0: 2.1 (LOW) — requires attacker-controlled input to reach the domain or path option.
Resources
Introduction commit: https://github.com/benoitc/hackney/commit/602d5c7f2ea4acbc83ed75230655d935a0750ebc Patch commit: https://github.com/benoitc/hackney/commit/8e02b99c28aea1b3fa2ddc0e66f51fe5bb0ac540
Summary
CVE-2026-47066 is an infinite loop (CWE-835) in hackney's Alt-Svc response header parser (src/hackneyaltsvc.erl). When an HTTP server returns an Alt-Svc header whose value begins with a non-token byte (e.g. !, @, =, ;), the parser enters a tight tail-recursive loop that pins an Erlang scheduler at 100% CPU and permanently hangs the calling connection process. Because the parser is invoked synchronously on every HTTP response, any attacker-controlled origin can trigger the hang with a single-byte header value.
Details
1. Parser dispatch
parseandcache/3 is called inside the hackney connection process on each HTTP response. It collects all Alt-Svc header values via collectaltsvcheaders/1, concatenates them, and passes the result to parse/1, which calls parseentries(Header, []).
2. Failed token consumption
parseentries/2 → parseentry/1 → parseprotocol/1 → parsetoken(Data, <<>>). The function parsetoken/2 pattern-matches leading bytes: alphanumeric, -, , whitespace, and comma all have explicit clauses. Any other byte (e.g. !) falls through to the catch-all:
erlang parsetoken(Rest, <<>>) -> {undefined, Rest}.
This returns the input unchanged — no byte is consumed.
3. No-progress loop
parseentry propagates {undefined, Rest} back to parseentries/2, which calls skipcomma(Rest). Because the first byte is not ,, skipcomma also returns Rest unchanged. parseentries then recurses with the identical buffer:
erlang parseentries(Data, Acc) % Data identical to previous iteration
Erlang tail recursion never preempts on a pure CPU loop, so the scheduler is pinned and the process never yields or returns.
4. Root cause
parseentries/2 has no guard that detects zero-byte progress after a failed parseentry call and no fallback to advance past the offending byte.
PoC
1. Start an HTTP server that responds with the header Alt-Svc: ! (any single non-token byte suffices). 2. Issue any HTTP GET request via hackney to that server: erlang hackney:request(get, "http://attacker.example/", [], <<>>, []) 3. Observe that the call never returns; the Erlang scheduler hosting the connection process is pinned at 100% CPU indefinitely.
Alternatively, call the parser directly: hackneyaltsvc:parse(<<"!">>) — the process hangs immediately.
Impact
Denial of service via unbounded CPU consumption. Any application using hackney 2.0.0-beta.1 through 4.0.0 that connects to attacker-controlled HTTP endpoints is affected. No authentication is required; a single response header byte is sufficient to hang the connection process. Fixed in hackney 4.0.1. CVSS v4.0 score: 8.7 (HIGH).
Resources
Introduction commit: https://github.com/benoitc/hackney/commit/408e5fe20302226ea8c74dde2bcbd452d712b5b2 Patch commit: https://github.com/benoitc/hackney/commit/e548aba1f97ffa3f4750da7b772998fb78c01894