identifier, policy, va: Remove/reject scope zone from IPv6 addresses (#8294)

Followup to #8293
Fixes #8292
This commit is contained in:
James Renken 2025-07-07 13:57:21 -07:00 committed by GitHub
parent 5b380adb53
commit c1ce0c83d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 51 additions and 2 deletions

View File

@ -118,7 +118,7 @@ func NewIP(ip netip.Addr) ACMEIdentifier {
// RFC 8738, Sec. 3: The identifier value MUST contain the textual form
// of the address as defined in RFC 1123, Sec. 2.1 for IPv4 and in RFC
// 5952, Sec. 4 for IPv6.
Value: ip.String(),
Value: ip.WithZone("").String(),
}
}

View File

@ -10,6 +10,39 @@ import (
"testing"
)
func TestNewIP(t *testing.T) {
cases := []struct {
name string
ip netip.Addr
want ACMEIdentifier
}{
{
name: "IPv4 address",
ip: netip.MustParseAddr("9.9.9.9"),
want: ACMEIdentifier{Type: TypeIP, Value: "9.9.9.9"},
},
{
name: "IPv6 address",
ip: netip.MustParseAddr("fe80::cafe"),
want: ACMEIdentifier{Type: TypeIP, Value: "fe80::cafe"},
},
{
name: "IPv6 address with scope zone",
ip: netip.MustParseAddr("fe80::cafe%lo"),
want: ACMEIdentifier{Type: TypeIP, Value: "fe80::cafe"},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := NewIP(tc.ip)
if got != tc.want {
t.Errorf("NewIP(%#v) = %#v, but want %#v", tc.ip, got, tc.want)
}
})
}
}
// TestFromX509 tests FromCert and FromCSR, which are fromX509's public
// wrappers.
func TestFromX509(t *testing.T) {

View File

@ -326,6 +326,7 @@ func ValidDomain(domain string) error {
// ValidIP checks that an IP address:
// - isn't empty
// - is an IPv4 or IPv6 address
// - doesn't contain a scope zone (RFC 4007)
// - isn't in an IANA special-purpose address registry
//
// It does NOT ensure that the IP address is absent from any PA blocked lists.
@ -340,7 +341,7 @@ func ValidIP(ip string) error {
// 5952, Sec. 4 for IPv6.") ParseAddr() will accept a non-compliant but
// otherwise valid string; String() will output a compliant string.
parsedIP, err := netip.ParseAddr(ip)
if err != nil || parsedIP.String() != ip {
if err != nil || parsedIP.WithZone("").String() != ip {
return errIPInvalid
}
@ -477,6 +478,7 @@ func (pa *AuthorityImpl) WillingToIssue(idents identifier.ACMEIdentifiers) error
//
// For IP identifiers:
// - MUST match the syntax of an IP address
// - MUST NOT contain a scope zone (RFC 4007)
// - MUST NOT be in an IANA special-purpose address registry
//
// If multiple identifiers are invalid, the error will contain suberrors

View File

@ -136,6 +136,8 @@ func TestWellFormedIdentifiers(t *testing.T) {
{identifier.ACMEIdentifier{Type: "ip", Value: `1.1.168.192.in-addr.arpa`}, errIPInvalid}, // reverse DNS
// Unexpected IPv6 variants
{identifier.ACMEIdentifier{Type: "ip", Value: `2602:80a:6000:abad:cafe::1%lo`}, errIPInvalid}, // scope zone (RFC 4007)
{identifier.ACMEIdentifier{Type: "ip", Value: `2602:80a:6000:abad:cafe::1%`}, errIPInvalid}, // empty scope zone (RFC 4007)
{identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:c0ff:ee:a:bad:deed:ffff`}, errIPInvalid}, // extra octet
{identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:c0ff:ee:a:bad:mead`}, errIPInvalid}, // character out of range
{identifier.ACMEIdentifier{Type: "ip", Value: `2001:db8::/32`}, errIPInvalid}, // with CIDR

View File

@ -338,6 +338,10 @@ func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (iden
reqIP, err := netip.ParseAddr(reqHost)
if err == nil {
// Reject IPv6 addresses with a scope zone (RFCs 4007 & 6874)
if reqIP.Zone() != "" {
return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError("Invalid host in redirect target: contains scope zone")
}
err := va.isReservedIPFunc(reqIP)
if err != nil {
return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError("Invalid host in redirect target: %s", err)

View File

@ -360,6 +360,14 @@ func TestExtractRequestTarget(t *testing.T) {
ExpectedError: fmt.Errorf("Invalid host in redirect target: " +
"IP address is in a reserved address block: [RFC9637]: Documentation"),
},
{
Name: "bare IPv6, scope zone",
Req: &http.Request{
URL: mustURL("http://[::1%25lo]"),
},
ExpectedError: fmt.Errorf("Invalid host in redirect target: " +
"contains scope zone"),
},
{
Name: "valid HTTP redirect, explicit port",
Req: &http.Request{