From 267bf8b639f43a4c2fe26b5ff8b4d49a20ac70d8 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Mon, 9 Mar 2026 11:50:44 +1300 Subject: [PATCH] Security: Enhance CORS origin handling to respect host:port distinctions --- server/cors.go | 15 ++++++++++----- server/cors_test.go | 7 +++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/server/cors.go b/server/cors.go index 934672e..07ce30e 100644 --- a/server/cors.go +++ b/server/cors.go @@ -48,17 +48,19 @@ func corsOriginAccessControl(r *http.Request) bool { } _, allAllowed := corsAllowOrigins["*"] - // allow same origin || is "*" is defined as an origin + // allow same origin, or if "*" is defined as an origin if asciiFoldString(u.Host) == asciiFoldString(r.Host) || allAllowed { return true } - originHostFold := asciiFoldString(u.Hostname()) + // match on full host:port so that example.com:8080 is not admitted + // by an allowlist entry for example.com (standard port 80/443). + originHostFold := asciiFoldString(u.Host) if corsAllowOrigins[originHostFold] { return true } - logger.Log().Warnf("[cors] blocking request from unauthorized origin: %s", u.Hostname()) + logger.Log().Warnf("[cors] blocking request from unauthorized origin: %s", u.Host) return false } @@ -67,7 +69,8 @@ func corsOriginAccessControl(r *http.Request) bool { } // SetCORSOrigins sets the allowed CORS origins from a comma-separated string. -// It does not consider port or protocol, only the hostname. +// Origins are matched on the full host:port, so example.com and example.com:8080 +// are treated as distinct origins. func setCORSOrigins() { corsAllowOrigins = make(map[string]bool) @@ -120,7 +123,9 @@ func extractOrigins(str string) []string { continue } - origins = append(origins, u.Hostname()) + // Store host:port so port differences are respected. + // u.Host equals u.Hostname() when no port is present. + origins = append(origins, u.Host) } } diff --git a/server/cors_test.go b/server/cors_test.go index a272676..5da9ed7 100644 --- a/server/cors_test.go +++ b/server/cors_test.go @@ -39,7 +39,7 @@ func TestExtractOrigins(t *testing.T) { { name: "mixed protocols", input: "http://example.com,https://foo.com:8080", - expected: []string{"example.com", "foo.com"}, + expected: []string{"example.com", "foo.com:8080"}, }, { @@ -78,7 +78,10 @@ func TestCorsOriginAccessControl(t *testing.T) { allow bool }{ {"no origin header", "", "example.com", true}, - {"allowed origin", "http://example.com:1234", "mailpit.local", true}, + // example.com:1234 must NOT be admitted by an allowlist entry for example.com (different port) + {"allowed origin", "http://example.com:1234", "mailpit.local", false}, + {"allowed origin", "http://example.com:1234", "example.com", false}, + {"allowed origin", "http://example.com:1234", "example.com:1234", true}, {"not allowed origin", "http://notallowed.com", "mailpit.local", false}, {"allowed by hostname", "http://foo.com", "mailpit.local", true}, {"ascii fold: allowed origin uppercase", "HTTP://EXAMPLE.COM", "mailpit.local", true},