Summary
hackney_url:normalize/2 URL-decodes the host component of a parsed URL, but the caller's SSRF allowlist runs before normalization using OTP's uri_string:parse/1 and inet:parse_address/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/hackney_url.erl (lines 161–186), normalize/2 checks whether the parsed host is already a dotted-quad or IPv6 literal via inet_parse: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:
Host1 = binary_to_list(
urldecode(unicode:characters_to_binary(Host0))
),
The decoded host ("127.0.0.1") replaces the original in the returned #hackney_url{} record. hackney:request/5 at src/hackney.erl:463 always calls normalize/2, so the decoded host is what do_dispatch/1 and add_host_header/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 parse_url/1 and did not touch normalize/2.
PoC
- Validate the URL with the canonical Erlang SSRF allowlist:
uri_string:parse/1 returns host <<"%31%32%37%2E%30%2E%30%2E%31">>, inet:parse_address/1 returns {error, einval}, so the allowlist accepts it.
- Pass the same URL to
hackney:get/1.
- 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
References
Summary
hackney_url:normalize/2URL-decodes the host component of a parsed URL, but the caller's SSRF allowlist runs before normalization using OTP'suri_string:parse/1andinet:parse_address/1, neither of which decodes percent-escapes in hostnames. A URL likehttp://%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 to127.0.0.1and connects to loopback. Becausehackney:request/5always callsnormalize/2with 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/hackney_url.erl(lines 161–186),normalize/2checks whether the parsed host is already a dotted-quad or IPv6 literal viainet_parse:address/1. Percent-encoded forms like%31%32%37%2E%30%2E%30%2E%31fail that check and fall into the catch-all branch, whereurldecode/1decodes the host before passing it to IDNA conversion:The decoded host (
"127.0.0.1") replaces the original in the returned#hackney_url{}record.hackney:request/5atsrc/hackney.erl:463always callsnormalize/2, so the decoded host is whatdo_dispatch/1andadd_host_header/2ultimately use. The on-wireHost: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 anylocalhostadmin endpoint. The 1.21.0 patch for CVE-2025-1211 fixed a separate differential inparse_url/1and did not touchnormalize/2.PoC
uri_string:parse/1returns host<<"%31%32%37%2E%30%2E%30%2E%31">>,inet:parse_address/1returns{error, einval}, so the allowlist accepts it.hackney:get/1.normalize/2decodes the host to"127.0.0.1"and connects to127.0.0.1:80. The internal service receives the request withHost: 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,
localhostadmin interfaces, and RFC1918 backends. CVSS v4.0: 6.9 (MEDIUM).Resources
References