mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-06-01 19:01:37 +00:00
feat(oauth2server): accept loopback redirect URIs
Previously the OAuth server rejected every redirect_uri that did not start with a vikunja- custom scheme. Native apps that cannot register a custom scheme (e.g. CLIs, desktop tools) need loopback redirects per RFC 8252, so also allow http://localhost, http://127.0.0.1 and http://[::1] (any port). Non-loopback http:// and https:// targets remain rejected. https://claude.ai/code/session_01LsTDrCJ7trE6WQ4FYf78UB
This commit is contained in:
@@ -21,15 +21,25 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ValidateRedirectURI checks that the redirect_uri uses a scheme starting with
|
||||
// "vikunja-". This allowlists only Vikunja native app schemes (e.g.
|
||||
// vikunja-flutter://callback) and rejects dangerous schemes like javascript:,
|
||||
// data:, http:, https:, etc.
|
||||
// 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.
|
||||
func ValidateRedirectURI(redirectURI string) bool {
|
||||
u, err := url.Parse(redirectURI)
|
||||
if err != nil || u.Scheme == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.HasPrefix(u.Scheme, "vikunja-")
|
||||
if strings.HasPrefix(u.Scheme, "vikunja-") {
|
||||
return true
|
||||
}
|
||||
|
||||
if u.Scheme == "http" {
|
||||
host := u.Hostname()
|
||||
return host == "localhost" || host == "127.0.0.1" || host == "::1"
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -29,11 +29,32 @@ func TestValidateRedirectURI(t *testing.T) {
|
||||
t.Run("accepts vikunja-desktop scheme", func(t *testing.T) {
|
||||
assert.True(t, ValidateRedirectURI("vikunja-desktop://auth"))
|
||||
})
|
||||
t.Run("accepts http localhost", func(t *testing.T) {
|
||||
assert.True(t, ValidateRedirectURI("http://localhost/callback"))
|
||||
})
|
||||
t.Run("accepts http localhost with port", func(t *testing.T) {
|
||||
assert.True(t, ValidateRedirectURI("http://localhost:8080/callback"))
|
||||
})
|
||||
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 http ipv6 loopback", func(t *testing.T) {
|
||||
assert.True(t, ValidateRedirectURI("http://[::1]:8080/callback"))
|
||||
})
|
||||
t.Run("rejects https scheme", func(t *testing.T) {
|
||||
assert.False(t, ValidateRedirectURI("https://evil.com/callback"))
|
||||
})
|
||||
t.Run("rejects http scheme", func(t *testing.T) {
|
||||
assert.False(t, ValidateRedirectURI("http://localhost/callback"))
|
||||
t.Run("rejects https localhost", func(t *testing.T) {
|
||||
assert.False(t, ValidateRedirectURI("https://localhost/callback"))
|
||||
})
|
||||
t.Run("rejects non-loopback http", func(t *testing.T) {
|
||||
assert.False(t, ValidateRedirectURI("http://evil.com/callback"))
|
||||
})
|
||||
t.Run("rejects http with localhost in path", func(t *testing.T) {
|
||||
assert.False(t, ValidateRedirectURI("http://evil.com/localhost"))
|
||||
})
|
||||
t.Run("rejects http with localhost subdomain trick", func(t *testing.T) {
|
||||
assert.False(t, ValidateRedirectURI("http://localhost.evil.com/callback"))
|
||||
})
|
||||
t.Run("rejects javascript scheme", func(t *testing.T) {
|
||||
assert.False(t, ValidateRedirectURI("javascript:alert(1)"))
|
||||
|
||||
Reference in New Issue
Block a user