diff --git a/pkg/gotenberg/outbound.go b/pkg/gotenberg/outbound.go index 2a771ba7..4e2a726c 100644 --- a/pkg/gotenberg/outbound.go +++ b/pkg/gotenberg/outbound.go @@ -109,11 +109,18 @@ var nonPublicIPv4Prefixes = []netip.Prefix{ // the prefixes themselves are deprecated or translation-only. See // [nonPublicIPv6Prefixes] and [nonPublicIPv4Prefixes] for the full lists // and rationale. +// +// An IPv6 zone identifier is ignored, so [::%1] classifies the same as [::]. func IsPublicIP(addr netip.Addr) bool { if !addr.IsValid() { return false } - addr = addr.Unmap() + // A zone does not change where a non-link-local address routes, but + // [netip.Prefix.Contains] never matches a zoned address and + // [netip.Addr.IsUnspecified] compares the zone too. Keeping it would let + // http://[::%251]/ or any zoned address in the prefixes below pass as + // public. + addr = addr.WithZone("").Unmap() switch { case addr.IsLoopback(), addr.IsPrivate(), diff --git a/pkg/gotenberg/outbound_test.go b/pkg/gotenberg/outbound_test.go index ec237a00..733b2280 100644 --- a/pkg/gotenberg/outbound_test.go +++ b/pkg/gotenberg/outbound_test.go @@ -103,6 +103,23 @@ func TestIsPublicIP(t *testing.T) { // Discard prefix (RFC 6666). {"100::1", false}, + + // A zone identifier must not change the classification. + {"::%1", false}, + {"::%lo", false}, + {"::1%1", false}, + {"fe80::1%eth0", false}, + {"fc00::1%1", false}, + {"::ffff:127.0.0.1%1", false}, + {"fec0::1%eth0", false}, + {"2002:a9fe:a9fe::%1", false}, + {"2001:0:abcd:ef12:3456:7890:a9fe:a9fe%1", false}, + {"64:ff9b::a9fe:a9fe%1", false}, + {"64:ff9b:1::a9fe:a9fe%1", false}, + {"::a9fe:a9fe%1", false}, + {"2001:db8::1%1", false}, + {"100::1%1", false}, + {"2606:4700:4700::1111%1", true}, } { t.Run(tc.addr, func(t *testing.T) { addr, err := netip.ParseAddr(tc.addr) @@ -304,6 +321,22 @@ func TestFilterOutboundURL(t *testing.T) { expectErr: true, expectIs: ErrFiltered, }, + { + scenario: "zoned unspecified address blocked with deny-private-ips", + rawURL: "http://[::%251]:9999/", + deny: defaultDeny, + opts: []DecideOption{WithDenyPrivateIPs(true)}, + expectErr: true, + expectIs: ErrFiltered, + }, + { + scenario: "zoned site-local address blocked with deny-private-ips", + rawURL: "http://[fec0:1234::3%25eth0]:8080/", + deny: defaultDeny, + opts: []DecideOption{WithDenyPrivateIPs(true)}, + expectErr: true, + expectIs: ErrFiltered, + }, } { t.Run(tc.scenario, func(t *testing.T) { if tc.stub != nil {