GHSA-pj7v-xfvx-wmjq
Hackney has SSRF allowlist bypass in hackney_url:normalize/2 via percent-encoded host
Blast Radius
hackneyReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Hex packages — download data is not available via public APIs for these ecosystems.
Description
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/1returns host<<"%31%32%37%2E%30%2E%30%2E%31">>,inet:parse_address/1returns{error, einval}, so the allowlist accepts it. - Pass the same URL to
hackney:get/1. - hackney's
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, localhost admin interfaces, and RFC1918 backends. CVSS v4.0: 6.9 (MEDIUM).
Resources
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 💧Hex | hackney | ≥ 0.13.0&&< 4.0.1 | 4.0.1 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for hackney. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update hackney to 4.0.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-pj7v-xfvx-wmjq is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-pj7v-xfvx-wmjq is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-pj7v-xfvx-wmjq. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-pj7v-xfvx-wmjq in your dependencies?
O3 detects GHSA-pj7v-xfvx-wmjq across Hex dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.