From 3bc798bc9d4d7b60c910f88ee3d057a55c259505 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 18 Mar 2026 03:05:10 +0000 Subject: [PATCH] fix: normalize IPv4-mapped cloud-metadata address to its IPv4 form before error reporting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IPv4-mapped cloud metadata (::ffff:169.254.169.254) previously fell through the IPv4-mapped IPv6 detection block and returned the generic private-IP error instead of the cloud-metadata error, making the two cases inconsistent - The IPv4-mapped error path used ip.String() (the raw ::ffff:… form) directly rather than sanitizeIPForError, potentially leaking the unsanitized IPv6 address in error messages visible to callers - Now extracts the IPv4 from the mapped address before both the cloud-metadata comparison and the sanitization call, so ::ffff:169.254.169.254 produces the same "access to cloud metadata endpoints is blocked" error as 169.254.169.254 and the error message is always sanitized through the shared helper - Updated the corresponding test to assert the cloud-metadata message and the absence of the raw IPv6 representation in the error text --- backend/internal/security/url_validator.go | 9 ++++++++- backend/internal/security/url_validator_test.go | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/backend/internal/security/url_validator.go b/backend/internal/security/url_validator.go index fa368925..0d8d3c62 100644 --- a/backend/internal/security/url_validator.go +++ b/backend/internal/security/url_validator.go @@ -294,7 +294,14 @@ func ValidateExternalURL(rawURL string, options ...ValidationOption) (string, er continue } if network.IsPrivateIP(ipv4) { - return "", fmt.Errorf("connection to private ip addresses is blocked for security (detected IPv4-mapped IPv6: %s)", ip.String()) + // Normalize to the extracted IPv4 for both the cloud-metadata special-case + // and sanitization, so ::ffff:169.254.169.254 produces the same error as + // 169.254.169.254 and doesn't leak the raw IPv6 form in messages. + sanitizedIPv4 := sanitizeIPForError(ipv4.String()) + if ipv4.String() == "169.254.169.254" { + return "", fmt.Errorf("access to cloud metadata endpoints is blocked for security (detected: %s)", sanitizedIPv4) + } + return "", fmt.Errorf("connection to private ip addresses is blocked for security (detected IPv4-mapped IPv6: %s)", sanitizedIPv4) } } diff --git a/backend/internal/security/url_validator_test.go b/backend/internal/security/url_validator_test.go index 240c4c50..6fe99a11 100644 --- a/backend/internal/security/url_validator_test.go +++ b/backend/internal/security/url_validator_test.go @@ -1175,7 +1175,8 @@ func TestValidateExternalURL_WithAllowRFC1918_IPv4MappedIPv6Allowed(t *testing.T func TestValidateExternalURL_WithAllowRFC1918_IPv4MappedMetadataBlocked(t *testing.T) { t.Parallel() - // ::ffff:169.254.169.254 maps to the cloud metadata IP; must stay blocked. + // ::ffff:169.254.169.254 maps to the cloud metadata IP; must stay blocked and + // produce the same cloud-metadata error as the non-mapped address. _, err := ValidateExternalURL( "http://[::ffff:169.254.169.254]", WithAllowHTTP(), @@ -1185,4 +1186,10 @@ func TestValidateExternalURL_WithAllowRFC1918_IPv4MappedMetadataBlocked(t *testi if err == nil { t.Fatal("expected IPv4-mapped metadata address to be blocked, got nil") } + if !strings.Contains(err.Error(), "cloud metadata") { + t.Errorf("expected cloud-metadata error for ::ffff:169.254.169.254, got: %v", err) + } + if strings.Contains(err.Error(), "ffff") { + t.Errorf("error message must not leak the raw IPv6 form, got: %v", err) + } }