1 Commits
Author SHA1 Message Date
clawbot 37bf7f4fbd server: limit wildcard CORS to the public routes (closes #100)
check / check (push) Successful in 1m40s
The CORS wildcard was global, so it also covered the Basic-Auth
protected /metrics, which REPO_POLICIES.md forbids, and it allowed
POST, PUT and DELETE, which no route serves, plus the Authorization
and X-CSRF-Token headers. CORS now sits on a router holding only the
public routes and allows GET and OPTIONS with the Accept and
Content-Type headers. /metrics gets no CORS at all.

Both are mounted routers rather than a Group: chi answers OPTIONS on
a Group's route with 405 before its middleware runs, and any method
/metrics does not register would otherwise fall through to the public
router. So every method on /metrics now meets Basic Auth first, and
/metrics/ is served like /metrics.

Model: opus-5-5
2026-10-01 18:28:41 +00:00
+34 -26
View File
@@ -75,6 +75,17 @@ func serve(
return rec
}
// publicPaths returns one path on each public route.
func publicPaths() []string {
return []string{
"/",
"/s/css/tailwind.min.css",
"/api/v1/status",
"/health",
"/.well-known/healthcheck",
}
}
// TestPublicRoutesAllowAnyOrigin checks that every public route answers
// a cross-origin GET with the CORS wildcard.
func TestPublicRoutesAllowAnyOrigin(t *testing.T) {
@@ -85,15 +96,7 @@ func TestPublicRoutesAllowAnyOrigin(t *testing.T) {
srv := routedServer(t)
paths := []string{
"/",
"/s/css/tailwind.min.css",
"/api/v1/status",
"/health",
"/.well-known/healthcheck",
}
for _, path := range paths {
for _, path := range publicPaths() {
rec := serve(srv, crossOriginRequest(t, http.MethodGet, path))
if rec.Code != http.StatusOK {
@@ -165,10 +168,13 @@ func TestMetricsHasNoCORS(t *testing.T) {
}
}
// TestPreflightAllowsOnlyWhatPublicRoutesServe checks what a public
// TestPreflightAllowsOnlyWhatPublicRoutesServe checks what each public
// route agrees to in a CORS preflight: GET, but not POST, PUT or
// DELETE, which no route serves, and not the Authorization or
// X-CSRF-Token headers, which no public route reads.
// X-CSRF-Token headers, which no public route reads. It checks every
// public route because one added with Get, such as /health, answers a
// preflight only while CORS is middleware of a whole router; in a
// Group, chi would answer it with 405 and no CORS headers.
func TestPreflightAllowsOnlyWhatPublicRoutesServe(t *testing.T) {
viper.Reset()
t.Setenv("DNSWATCHER_TARGETS", "example.com")
@@ -191,23 +197,25 @@ func TestPreflightAllowsOnlyWhatPublicRoutesServe(t *testing.T) {
{http.MethodGet, "X-CSRF-Token", false},
}
for _, tt := range tests {
rec := serve(srv, preflightRequest(
t, "/api/v1/status", tt.method, tt.headers,
))
for _, path := range publicPaths() {
for _, tt := range tests {
rec := serve(srv, preflightRequest(
t, path, tt.method, tt.headers,
))
want := ""
if tt.allowed {
want = tt.method
}
want := ""
if tt.allowed {
want = tt.method
}
got := rec.Header().Get("Access-Control-Allow-Methods")
if got != want {
t.Errorf(
"preflight for %s with headers %q: "+
"Access-Control-Allow-Methods = %q, want %q",
tt.method, tt.headers, got, want,
)
got := rec.Header().Get("Access-Control-Allow-Methods")
if got != want {
t.Errorf(
"preflight to %s for %s with headers %q: "+
"Access-Control-Allow-Methods = %q, want %q",
path, tt.method, tt.headers, got, want,
)
}
}
}
}