Validate every resolved address and bind it to the request (CVE-2026-77866, CVE-2026-77972) - #16
Open
DiedeClaessens wants to merge 2 commits into
Open
DiedeClaessens wants to merge 2 commits into
DiedeClaessens wants to merge 2 commits into
Conversation
Fixes CVE-2026-77866 and CVE-2026-77972, which affect every release. Only the first IPv4 address of a host was matched against the reserved ranges and the blocklist, and anything else matched nothing. A host rejected in its IPv4 form was accepted written as an IPv6 address, IPv6 entries in a blocklist never matched, and a host that resolved to no IPv4 address was accepted whatever it pointed at. The host is now resolved once and every address it has, in both families, must pass. IPv4-mapped (::ffff:10.0.0.1) and IPv4-compatible (::10.0.0.1) addresses are checked as the IPv4 address they carry, the reserved list covers the IPv6 blocks IANA marks as not globally reachable, and a host without any address is rejected with the new :unresolved_host rather than let through. The default resolver, SafeURL.DNS, looks up A and AAAA records and bounds each query, which was previously unbounded. Validation also returned a verdict and not the address it approved, so the hostname went to an HTTP client that resolved it a second time. A name under an attacker's control could answer with a public address for the check and an internal one for the request, and the same window opens on its own whenever a name legitimately resolves to different addresses across lookups. pin/2 returns the URL with the validated address in place of the host, plus the hostname, address, scheme and port, so a caller connects to what passed while the name still governs the Host header, the server name and the certificate check. The advisory names the HTTP clients this library ships, and both used the old pattern. SafeURL.HTTPoison.get/3 now pins the request and rebuilds hackney's TLS options from the hostname, because supplying :ssl_options replaces hackney's defaults rather than adding to them; a caller who sets :ssl_options or :insecure keeps their own setup, and validation options go under :safeurl. SafeURL.TeslaMiddleware pins any request without TLS and https on Tesla.Adapter.Hackney; with another adapter it validates but cannot pin, since keeping certificate verification pointed at the hostname is spelled differently by each one, and the docs say so. Both clients refuse :follow_redirect, given directly or through the adapter options, because a redirect names a destination that was never validated; hackney was asked to follow one and reached a link-local metadata address. With Tesla, placing the middleware after Tesla.Middleware.FollowRedirects validates every hop instead. The CI matrix moves to a runner image that still exists, and guides/migrating_to_1.1.md covers the URLs 1.0.0 accepted and this release rejects. The version stays on the 1.x line so that a "~> 1.0" dependency picks the fix up.
SafeURL.Client.hackney_ssl_options/1 calls :hackney_ssl.check_hostname_opts/1, which is exported but undocumented and does not exist before hackney 1.16.0. httpoison pulls a newer one in practice, but Tesla's hackney adapter allows ~> 1.6, so an https request pinned through the middleware could raise UndefinedFunctionError at request time instead of being sent. hackney joins the optional dependencies with that floor, so the resolver refuses the combination rather than the request. The package shipped mix.exs, lib and README.md only, while docs: extras points at the migration guides, so they were listed in the docs and missing from the tarball. Both guides ship now. Covers the client helpers too: the host header replaces one the caller set and carries the port when it is not the scheme default, the rebuilt TLS options verify against the hostname rather than the pinned address, and :follow_redirect raises.
Author
|
Also linked to #9 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes CVE-2026-77866 and CVE-2026-77972, which affect every release.
The holes
Only one address was judged.
validate/2took the first IPv4 address of a host and matched it against the reserved ranges and the blocklist; everything else matched nothing. So a host rejected in its IPv4 form passed written as an IPv6 address, IPv6 entries in a blocklist never matched, and a host that resolved to no A record was accepted whatever it pointed at.The verdict was not bound to a request.
validate/2returned:okand the caller handed the hostname to an HTTP client, which resolved it a second time. A name under an attacker's control answers with a public address for the check and an internal one for the request. The same window opens on its own whenever a name legitimately resolves to different addresses across lookups.The bundled clients followed redirects.
SafeURL.HTTPoison.get/3passed:follow_redirectto hackney, which then requested whatever destination the server named, unvalidated.What this does
::ffff:10.0.0.1) and IPv4-compatible (::10.0.0.1) addresses are checked as the IPv4 address they carry, and the reserved list now covers the IPv6 blocks IANA marks as not globally reachable.:unresolved_hostinstead of being let through.:dns_moduledefaults to the newSafeURL.DNS, which looks up A and AAAA records and bounds each query (DNS.resolve/4waits forever by default). Theresolve/1callback docs now say a resolver must return every address of every family.SafeURL.pin/2returns the URL with the validated address in place of the host, plushostname,address,schemeandport, so a caller connects to the address that passed while the name still governs theHostheader, SNI and certificate verification.validate/2andallowed?/2are unchanged in behaviour and now sit on top of it.SafeURL.HTTPoison.get/3pins the request and rebuilds hackney's TLS options from the hostname, since supplying:ssl_optionsreplaces hackney's defaults rather than adding to them. A caller who sets:ssl_optionsor:insecurekeeps their own setup. Validation options move under:safeurl.SafeURL.TeslaMiddlewarepins any request without TLS, andhttpsonTesla.Adapter.Hackney. With another adapter anhttpsrequest is validated but not pinned, because keeping certificate verification pointed at the hostname is spelled differently by each adapter; the moduledoc and the README say so.:follow_redirect, given directly or through the adapter options. With Tesla, placing the middleware afterTesla.Middleware.FollowRedirectsvalidates every hop, and there is a test for that.Breaking changes
guides/migrating_to_1.1.mdlists them: the new:unresolved_hosterror, the widened reserved list (192.0.0.0/29becomes IANA's192.0.0.0/24), the new default resolver, and:follow_redirectraising. Everything it now rejects was one of the holes. The version stays on the1.xline so{:safeurl, "~> 1.0"}picks the fix up.Notes for review
hackneyjoins the optional deps with a~> 1.16floor:SafeURL.Client.hackney_ssl_options/1calls:hackney_ssl.check_hostname_opts/1, which is exported but undocumented, and does not exist before 1.16.0. Without the floor, a Tesla user onhackney ~> 1.6would hitUndefinedFunctionErrorat request time. Happy to inline hackney's defaults instead if you would rather not lean on that function.guidesis added to the packagefiles, otherwise the migration guides are indocs: extrasbut not in the published tarball.ubuntu-22.04and the v4 actions:ubuntu-20.04is retired and its 1.14 entry paired it with OTP 23, which the current image does not ship. Happy to pull that out if you would rather take it on its own.mix testis green locally on Elixir 1.19.5 / OTP 28; the matrix in CI is what checks 1.14/OTP 24 and 1.17/OTP 27.Happy to split this further, or to cut the pinning API out into its own PR if you would rather land the address validation first.