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:
@@ -788,6 +788,7 @@ func (impl *Implm) Run(
|
|||||||
complianceportal_v1.MuxConfig{
|
complianceportal_v1.MuxConfig{
|
||||||
BaseURL: baseURL,
|
BaseURL: baseURL,
|
||||||
ExtraHeaderFields: impl.cfg.Api.ExtraHeaderFields,
|
ExtraHeaderFields: impl.cfg.Api.ExtraHeaderFields,
|
||||||
|
AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins,
|
||||||
Logger: l.Named("compliance-portal"),
|
Logger: l.Named("compliance-portal"),
|
||||||
IAM: iamService,
|
IAM: iamService,
|
||||||
Visitor: visitorService,
|
Visitor: visitorService,
|
||||||
|
|||||||
@@ -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) {
|
func (sr *SafeRedirect) Validate(ctx context.Context, redirectURL string) (string, bool) {
|
||||||
if redirectURL == "" {
|
if redirectURL == "" {
|
||||||
return "", false
|
return "", false
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -49,6 +48,7 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/probo"
|
"go.probo.inc/probo/pkg/probo"
|
||||||
"go.probo.inc/probo/pkg/resourcealias"
|
"go.probo.inc/probo/pkg/resourcealias"
|
||||||
"go.probo.inc/probo/pkg/riskmanagement"
|
"go.probo.inc/probo/pkg/riskmanagement"
|
||||||
|
"go.probo.inc/probo/pkg/saferedirect"
|
||||||
"go.probo.inc/probo/pkg/securecookie"
|
"go.probo.inc/probo/pkg/securecookie"
|
||||||
agent_v1 "go.probo.inc/probo/pkg/server/api/agent/v1"
|
agent_v1 "go.probo.inc/probo/pkg/server/api/agent/v1"
|
||||||
connect_v1 "go.probo.inc/probo/pkg/server/api/connect/v1"
|
connect_v1 "go.probo.inc/probo/pkg/server/api/connect/v1"
|
||||||
@@ -262,13 +262,11 @@ func NewServer(cfg Config) (*Server, error) {
|
|||||||
cfg.TokenSecret,
|
cfg.TokenSecret,
|
||||||
cfg.File,
|
cfg.File,
|
||||||
cfg.BaseURL,
|
cfg.BaseURL,
|
||||||
func(ctx context.Context, host string) bool {
|
saferedirect.Any(
|
||||||
if host == cfg.BaseURL.Host() {
|
saferedirect.StaticHosts(cfg.BaseURL.Host()),
|
||||||
return true
|
saferedirect.Origins(cfg.AllowedOrigins...),
|
||||||
}
|
cfg.Visitor.IsVerifiedRedirectHost,
|
||||||
|
),
|
||||||
return cfg.Visitor.IsVerifiedRedirectHost(ctx, host)
|
|
||||||
},
|
|
||||||
cfg.GraphQLLimits,
|
cfg.GraphQLLimits,
|
||||||
),
|
),
|
||||||
agentHandler: agent_v1.NewMux(
|
agentHandler: agent_v1.NewMux(
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
package complianceportal_v1
|
package complianceportal_v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
@@ -27,6 +26,7 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/iam"
|
"go.probo.inc/probo/pkg/iam"
|
||||||
"go.probo.inc/probo/pkg/mailman"
|
"go.probo.inc/probo/pkg/mailman"
|
||||||
"go.probo.inc/probo/pkg/resourcealias"
|
"go.probo.inc/probo/pkg/resourcealias"
|
||||||
|
"go.probo.inc/probo/pkg/saferedirect"
|
||||||
"go.probo.inc/probo/pkg/securecookie"
|
"go.probo.inc/probo/pkg/securecookie"
|
||||||
"go.probo.inc/probo/pkg/server"
|
"go.probo.inc/probo/pkg/server"
|
||||||
"go.probo.inc/probo/pkg/server/api/authn"
|
"go.probo.inc/probo/pkg/server/api/authn"
|
||||||
@@ -37,6 +37,7 @@ import (
|
|||||||
type MuxConfig struct {
|
type MuxConfig struct {
|
||||||
BaseURL *baseurl.BaseURL
|
BaseURL *baseurl.BaseURL
|
||||||
ExtraHeaderFields map[string]string
|
ExtraHeaderFields map[string]string
|
||||||
|
AllowedOrigins []string
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
IAM *iam.Service
|
IAM *iam.Service
|
||||||
Visitor *visitor.Service
|
Visitor *visitor.Service
|
||||||
@@ -66,9 +67,10 @@ func NewMux(cfg MuxConfig) (http.Handler, error) {
|
|||||||
r.Get("/robots.txt", markdownHandler.HandleRobotsTxt)
|
r.Get("/robots.txt", markdownHandler.HandleRobotsTxt)
|
||||||
r.Get("/sitemap.xml", markdownHandler.HandleSitemap)
|
r.Get("/sitemap.xml", markdownHandler.HandleSitemap)
|
||||||
|
|
||||||
allowedHost := func(ctx context.Context, host string) bool {
|
allowedHost := saferedirect.Any(
|
||||||
return cfg.Visitor.IsVerifiedRedirectHost(ctx, host)
|
saferedirect.Origins(cfg.AllowedOrigins...),
|
||||||
}
|
cfg.Visitor.IsVerifiedRedirectHost,
|
||||||
|
)
|
||||||
|
|
||||||
oauthInitiateHandler := NewOAuthInitiateHandler(
|
oauthInitiateHandler := NewOAuthInitiateHandler(
|
||||||
cfg.BaseURL,
|
cfg.BaseURL,
|
||||||
|
|||||||
Reference in New Issue
Block a user