The private-address check misses an IPv4 address embedded in an IPv6 one #297

Closed
opened 2026-10-16 04:49:04 +02:00 by clawbot · 2 comments
Collaborator

Where: internal/httpfetcher/httpfetcher.go, isPrivateIP, on next at efa431b. Found by the review of #296.

What is wrong: the check that keeps pixa from fetching internal addresses reads an IPv6 address as IPv4 only in the mapped form (::ffff:a.b.c.d). An IPv4 address embedded another way is judged as IPv6 and passes: the IPv4-compatible form ::127.0.0.1 (::/96), and the NAT64 form 64:ff9b::10.0.0.1 (64:ff9b::/96), which on a network with NAT64 reaches that IPv4 address.

How it fails: an outsider requests an image whose host is such an address (or a name that resolves to one), and on a network that routes it, pixa fetches from an internal host.

Severity: medium (it weakens the check that blocks fetching internal addresses).

Definition of done:

  1. An IPv6 address in ::/96 or 64:ff9b::/96 is judged by the IPv4 address it embeds, so one embedding a private, loopback or link-local IPv4 address is refused like that IPv4 address; other IPv6 addresses are judged as before.
  2. Tests first, for the check itself and through the router, for [::127.0.0.1], [::10.0.0.1] and [64:ff9b::7f00:1]; make check green.

Package: internal/httpfetcher.

Model: opus-5-5

Where: `internal/httpfetcher/httpfetcher.go`, `isPrivateIP`, on `next` at `efa431b`. Found by the review of https://git.eeqj.de/sneak/pixa/pulls/296. What is wrong: the check that keeps pixa from fetching internal addresses reads an IPv6 address as IPv4 only in the mapped form (`::ffff:a.b.c.d`). An IPv4 address embedded another way is judged as IPv6 and passes: the IPv4-compatible form `::127.0.0.1` (`::/96`), and the NAT64 form `64:ff9b::10.0.0.1` (`64:ff9b::/96`), which on a network with NAT64 reaches that IPv4 address. How it fails: an outsider requests an image whose host is such an address (or a name that resolves to one), and on a network that routes it, pixa fetches from an internal host. Severity: medium (it weakens the check that blocks fetching internal addresses). Definition of done: 1. An IPv6 address in `::/96` or `64:ff9b::/96` is judged by the IPv4 address it embeds, so one embedding a private, loopback or link-local IPv4 address is refused like that IPv4 address; other IPv6 addresses are judged as before. 2. Tests first, for the check itself and through the router, for `[::127.0.0.1]`, `[::10.0.0.1]` and `[64:ff9b::7f00:1]`; `make check` green. Package: `internal/httpfetcher`. Model: opus-5-5
clawbot self-assigned this 2026-10-16 04:49:15 +02:00
Author
Collaborator

Plan: isPrivateIP in internal/httpfetcher/httpfetcher.go will judge an IPv6 address in ::/96 (an IPv4 address written in the old IPv4-compatible form, such as ::127.0.0.1) by the IPv4 address in its last four bytes, checked with a net/netip prefix. One that holds a private, loopback or link-local IPv4 address is then refused as that address is. Every other IPv6 address is checked as it is now.

The NAT64 range 64:ff9b::/96 needs no code change. On next it is already one of the built-in blocked ranges, so every address in it is refused whatever IPv4 address it holds, and README.md lists NAT64 among the ranges always refused. Judging a NAT64 address by its IPv4 address alone would let one holding a public IPv4 address through, which loosens the check, so I leave that range as it is.

The tests go in first, in a commit of their own. In internal/httpfetcher they cover the check itself, and in internal/server they go through the router: [::127.0.0.1], [::10.0.0.1] and [64:ff9b::7f00:1] answer 403 and the upstream gets no request, while ::8.8.8.8 still passes. No existing test changes.

Model: opus-5-5

Plan: `isPrivateIP` in `internal/httpfetcher/httpfetcher.go` will judge an IPv6 address in `::/96` (an IPv4 address written in the old IPv4-compatible form, such as `::127.0.0.1`) by the IPv4 address in its last four bytes, checked with a `net/netip` prefix. One that holds a private, loopback or link-local IPv4 address is then refused as that address is. Every other IPv6 address is checked as it is now. The NAT64 range `64:ff9b::/96` needs no code change. On `next` it is already one of the built-in blocked ranges, so every address in it is refused whatever IPv4 address it holds, and `README.md` lists NAT64 among the ranges always refused. Judging a NAT64 address by its IPv4 address alone would let one holding a public IPv4 address through, which loosens the check, so I leave that range as it is. The tests go in first, in a commit of their own. In `internal/httpfetcher` they cover the check itself, and in `internal/server` they go through the router: `[::127.0.0.1]`, `[::10.0.0.1]` and `[64:ff9b::7f00:1]` answer 403 and the upstream gets no request, while `::8.8.8.8` still passes. No existing test changes. Model: opus-5-5
Author
Collaborator

Built as planned in #298. One difference from the plan: the router test covers only the three refused hosts, and that ::8.8.8.8 passes is tested on the check itself.

Model: opus-5-5

Built as planned in https://git.eeqj.de/sneak/pixa/pulls/298. One difference from the plan: the router test covers only the three refused hosts, and that `::8.8.8.8` passes is tested on the check itself. Model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/pixa#297