From 9714296efb3768e4f29479f48628318565a3bed7 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Mon, 13 Jul 2026 11:05:24 +0200 Subject: [PATCH] fix: block link-local addresses in SSRF protection --- backend/internal/utils/ip_util.go | 13 ++++++- backend/internal/utils/ip_util_test.go | 49 ++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/backend/internal/utils/ip_util.go b/backend/internal/utils/ip_util.go index 4136658f..d91850ad 100644 --- a/backend/internal/utils/ip_util.go +++ b/backend/internal/utils/ip_util.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net" + "net/netip" "net/url" "strings" @@ -56,7 +57,17 @@ func IsTailscaleIP(ip net.IP) bool { } func IsPrivateIP(ip net.IP) bool { - return IsLocalhostIP(ip) || IsPrivateLanIP(ip) || IsTailscaleIP(ip) || IsLocalIPv6(ip) + if IsLocalhostIP(ip) || IsPrivateLanIP(ip) || IsTailscaleIP(ip) || IsLocalIPv6(ip) { + return true + } + + addr, ok := netip.AddrFromSlice(ip) + if !ok { + return false + } + + addr = addr.Unmap() + return addr.IsLoopback() || addr.IsPrivate() || addr.IsLinkLocalUnicast() || addr.IsLinkLocalMulticast() || addr.IsUnspecified() } func IsURLPrivate(ctx context.Context, u *url.URL) (bool, error) { diff --git a/backend/internal/utils/ip_util_test.go b/backend/internal/utils/ip_util_test.go index 5da1eb68..05d52a6f 100644 --- a/backend/internal/utils/ip_util_test.go +++ b/backend/internal/utils/ip_util_test.go @@ -110,12 +110,19 @@ func TestIsPrivateIP(t *testing.T) { ip string expected bool }{ - {"127.0.0.1", true}, // localhost - {"192.168.1.1", true}, // private LAN - {"100.64.0.1", true}, // Tailscale - {"fd00::1", true}, // local IPv6 - {"8.8.8.8", false}, // public IPv4 - {"2001:4860:4860::8888", false}, // public IPv6 + {"127.0.0.1", true}, // localhost + {"192.168.1.1", true}, // private LAN + {"100.64.0.1", true}, // Tailscale + {"169.254.169.254", true}, // IPv4 link-local + {"169.254.170.2", true}, // IPv4 link-local + {"::ffff:169.254.169.254", true}, // IPv4-mapped link-local + {"fe80::1", true}, // IPv6 link-local + {"ff02::1", true}, // IPv6 link-local multicast + {"0.0.0.0", true}, // IPv4 unspecified + {"::", true}, // IPv6 unspecified + {"fd00::1", true}, // private IPv6 + {"8.8.8.8", false}, // public IPv4 + {"2001:4860:4860::8888", false}, // public IPv6 } for _, tt := range tests { @@ -231,6 +238,36 @@ func TestIsURLPrivate(t *testing.T) { expectPriv: true, expectError: false, }, + { + name: "IPv4 link-local metadata IP", + urlStr: "http://169.254.169.254", + expectPriv: true, + expectError: false, + }, + { + name: "IPv4-mapped link-local metadata IP", + urlStr: "http://[::ffff:169.254.169.254]", + expectPriv: true, + expectError: false, + }, + { + name: "IPv6 link-local IP", + urlStr: "http://[fe80::1]", + expectPriv: true, + expectError: false, + }, + { + name: "IPv4 unspecified IP", + urlStr: "http://0.0.0.0", + expectPriv: true, + expectError: false, + }, + { + name: "IPv6 unspecified IP", + urlStr: "http://[::]", + expectPriv: true, + expectError: false, + }, { name: "public IP - Google DNS", urlStr: "http://8.8.8.8",