Allow CORS origins in safeRedirect hosts

Local Vite continue URLs use absolute localhost origins that never
pass verified custom-domain checks. Reuse AllowedOrigins so post-auth
redirects work in dev without disabling Validate.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-29 16:42:04 +02:00
parent 69721c6afd
commit ed297ccc9e
5 changed files with 127 additions and 12 deletions

View File

@@ -55,6 +55,37 @@ func StaticHosts(hosts ...string) AllowedHostFunc {
}
}
// Origins returns an AllowedHost function that matches hosts extracted from
// absolute origin URLs (for example CORS allowed-origins). Invalid or empty
// origins are ignored. The host includes the port when present.
func Origins(origins ...string) AllowedHostFunc {
hosts := make([]string, 0, len(origins))
for _, origin := range origins {
parsed, err := url.Parse(origin)
if err != nil || parsed.Host == "" {
continue
}
hosts = append(hosts, parsed.Host)
}
return StaticHosts(hosts...)
}
// Any returns an AllowedHost function that allows a host when any of the
// provided functions allow it. Nil functions are skipped.
func Any(fns ...AllowedHostFunc) AllowedHostFunc {
return func(ctx context.Context, host string) bool {
for _, fn := range fns {
if fn != nil && fn(ctx, host) {
return true
}
}
return false
}
}
func (sr *SafeRedirect) Validate(ctx context.Context, redirectURL string) (string, bool) {
if redirectURL == "" {
return "", false

View File

@@ -439,3 +439,86 @@ func TestStaticHosts(t *testing.T) {
}
})
}
func TestOrigins(t *testing.T) {
t.Parallel()
t.Run("extracts hosts including ports", func(t *testing.T) {
t.Parallel()
fn := saferedirect.Origins(
"http://localhost:5174",
"https://app.example.com",
)
if !fn(context.Background(), "localhost:5174") {
t.Error("expected localhost:5174 to be allowed")
}
if !fn(context.Background(), "app.example.com") {
t.Error("expected app.example.com to be allowed")
}
if fn(context.Background(), "evil.com") {
t.Error("expected evil.com to be rejected")
}
})
t.Run("ignores invalid and empty origins", func(t *testing.T) {
t.Parallel()
fn := saferedirect.Origins("", "not a url", "http://localhost:5173")
if !fn(context.Background(), "localhost:5173") {
t.Error("expected localhost:5173 to be allowed")
}
if fn(context.Background(), "") {
t.Error("expected empty host to be rejected")
}
})
}
func TestAny(t *testing.T) {
t.Parallel()
t.Run("allows when any function matches", func(t *testing.T) {
t.Parallel()
fn := saferedirect.Any(
saferedirect.StaticHosts("a.com"),
saferedirect.StaticHosts("b.com"),
)
if !fn(context.Background(), "a.com") {
t.Error("expected a.com to be allowed")
}
if !fn(context.Background(), "b.com") {
t.Error("expected b.com to be allowed")
}
if fn(context.Background(), "c.com") {
t.Error("expected c.com to be rejected")
}
})
t.Run("skips nil functions", func(t *testing.T) {
t.Parallel()
fn := saferedirect.Any(nil, saferedirect.StaticHosts("example.com"), nil)
if !fn(context.Background(), "example.com") {
t.Error("expected example.com to be allowed")
}
if fn(context.Background(), "other.com") {
t.Error("expected other.com to be rejected")
}
})
t.Run("rejects when no functions provided", func(t *testing.T) {
t.Parallel()
fn := saferedirect.Any()
if fn(context.Background(), "example.com") {
t.Error("expected example.com to be rejected")
}
})
}