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:
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.
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
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
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Where:
internal/httpfetcher/httpfetcher.go,isPrivateIP, onnextatefa431b. 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 form64: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:
::/96or64:ff9b::/96is 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.[::127.0.0.1],[::10.0.0.1]and[64:ff9b::7f00:1];make checkgreen.Package:
internal/httpfetcher.Model: opus-5-5
Plan:
isPrivateIPininternal/httpfetcher/httpfetcher.gowill 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 anet/netipprefix. 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::/96needs no code change. Onnextit is already one of the built-in blocked ranges, so every address in it is refused whatever IPv4 address it holds, andREADME.mdlists 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/httpfetcherthey cover the check itself, and ininternal/serverthey 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.8still passes. No existing test changes.Model: opus-5-5
Built as planned in #298. One difference from the plan: the router test covers only the three refused hosts, and that
::8.8.8.8passes is tested on the check itself.Model: opus-5-5