Refuse an IPv6 address that holds an internal IPv4 address (closes #297) #298

Merged
clawbot merged 2 commits from issue-297-embedded-ipv4-private-check into next 2026-10-16 05:31:56 +02:00
2 changed files with 15 additions and 0 deletions
Showing only changes of commit 1631f92e2f - Show all commits
+8
View File
@@ -30,6 +30,14 @@ P2: security: per-origin rate limiting
# Completed Steps
- 2026-10-16 P1 security bug: the check that keeps pixa from fetching internal
addresses judges an IPv6 address in the IPv4-compatible form (`::/96`), such
as `::127.0.0.1` or `::10.0.0.1`, by the IPv4 address in its last four bytes
(closes #297). It read only the mapped form (`::ffff:a.b.c.d`) as IPv4, so a
source host such as `[::127.0.0.1]` was checked as a public IPv6 address and
fetched, which on a network that routes it reaches the internal IPv4 host; it
is now refused with 403. The NAT64 form (`64:ff9b::/96`) was already refused
as a whole, as one of the built-in blocked ranges, and still is.
- 2026-10-16 a source host that is an IPv6 address in brackets with no port,
such as `[2001:db8::a]`, is fetched like one with a port, through `/v1/image/`
and through an encrypted URL (closes #295). The fetcher's check of the source
+7
View File
@@ -543,6 +543,13 @@ func isPrivateIP(ip net.IP) bool {
return true
}
// An IPv4-compatible IPv6 address (in ::/96, such as ::127.0.0.1) holds
// an IPv4 address in its last four bytes and is judged as that address
if addr, ok := netip.AddrFromSlice(ip); ok &&
netip.MustParsePrefix("::/96").Contains(addr) {
return isPrivateIP(ip[12:])
}
// Check for loopback
if ip.IsLoopback() {
return true