From aa1956e1aabc1a737bc3ead184e91ad063133bff Mon Sep 17 00:00:00 2001 From: Tink bot Date: Thu, 7 May 2026 21:52:18 +0000 Subject: [PATCH] fix(oauth2server): accept all loopback redirect forms Hardcoding the three exact strings localhost / 127.0.0.1 / ::1 rejected legitimate loopback redirects like 127.0.0.2:1234 (anywhere in 127.0.0.0/8) or [0:0:0:0:0:0:0:1]:1234 (expanded IPv6 loopback). Use net.IP.IsLoopback() to cover the full loopback ranges, and match "localhost" case-insensitively. 0.0.0.0 stays rejected as it is not a loopback address. https://claude.ai/code/session_01LsTDrCJ7trE6WQ4FYf78UB --- pkg/modules/auth/oauth2server/client.go | 14 +++++++++++--- pkg/modules/auth/oauth2server/client_test.go | 12 ++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/pkg/modules/auth/oauth2server/client.go b/pkg/modules/auth/oauth2server/client.go index cad975731..1ec866733 100644 --- a/pkg/modules/auth/oauth2server/client.go +++ b/pkg/modules/auth/oauth2server/client.go @@ -17,6 +17,7 @@ package oauth2server import ( + "net" "net/url" "strings" ) @@ -24,8 +25,10 @@ import ( // ValidateRedirectURI checks that the redirect_uri is either a Vikunja native // app scheme (e.g. vikunja-flutter://callback) or a loopback http URL as // recommended by RFC 8252 for native apps that cannot register a custom -// scheme. Dangerous schemes like javascript:, data:, https://, or non-loopback -// http:// targets are rejected. +// scheme. Any address in 127.0.0.0/8, the IPv6 loopback (::1, in any +// notation), and the literal hostname "localhost" are accepted; dangerous +// schemes like javascript:, data:, https://, or non-loopback http:// targets +// are rejected. func ValidateRedirectURI(redirectURI string) bool { u, err := url.Parse(redirectURI) if err != nil || u.Scheme == "" { @@ -38,7 +41,12 @@ func ValidateRedirectURI(redirectURI string) bool { if u.Scheme == "http" { host := u.Hostname() - return host == "localhost" || host == "127.0.0.1" || host == "::1" + if strings.EqualFold(host, "localhost") { + return true + } + if ip := net.ParseIP(host); ip != nil && ip.IsLoopback() { + return true + } } return false diff --git a/pkg/modules/auth/oauth2server/client_test.go b/pkg/modules/auth/oauth2server/client_test.go index 40d602595..c1626f100 100644 --- a/pkg/modules/auth/oauth2server/client_test.go +++ b/pkg/modules/auth/oauth2server/client_test.go @@ -38,9 +38,21 @@ func TestValidateRedirectURI(t *testing.T) { t.Run("accepts http 127.0.0.1", func(t *testing.T) { assert.True(t, ValidateRedirectURI("http://127.0.0.1:8080/callback")) }) + t.Run("accepts other 127.0.0.0/8 loopback addresses", func(t *testing.T) { + assert.True(t, ValidateRedirectURI("http://127.0.0.2:1234/callback")) + }) t.Run("accepts http ipv6 loopback", func(t *testing.T) { assert.True(t, ValidateRedirectURI("http://[::1]:8080/callback")) }) + t.Run("accepts expanded ipv6 loopback", func(t *testing.T) { + assert.True(t, ValidateRedirectURI("http://[0:0:0:0:0:0:0:1]:1234/callback")) + }) + t.Run("accepts localhost case-insensitively", func(t *testing.T) { + assert.True(t, ValidateRedirectURI("http://LocalHost:8080/callback")) + }) + t.Run("rejects 0.0.0.0", func(t *testing.T) { + assert.False(t, ValidateRedirectURI("http://0.0.0.0:8080/callback")) + }) t.Run("rejects https scheme", func(t *testing.T) { assert.False(t, ValidateRedirectURI("https://evil.com/callback")) })