server: limit wildcard CORS to the public routes (closes #100) #172

Merged
clawbot merged 1 commits from issue-100-cors-scope into next 2026-10-01 21:05:43 +02:00
Collaborator

Implements #100.

Wildcard CORS now covers only the public routes (/, /s/..., /api/v1/status, /health, /.well-known/healthcheck). /metrics gets no CORS at all, the option the issue prefers. CORS allows GET and OPTIONS with the Accept and Content-Type headers; POST, PUT, DELETE, Authorization and X-CSRF-Token are gone.

What the diff does not show:

  • The public routes sit on their own router mounted at /, not in a Group: chi answers OPTIONS on a group's route with 405 before the group's middleware runs, so preflight requests would no longer get a CORS answer.
  • /metrics is a mounted router too. chi hands a method that a path does not register to the / mount, so with a plain Get an OPTIONS /metrics preflight got the wildcard. The new tests in internal/server/routes_test.go cover both: they send preflight requests to every public route, those added with Get included, and to /metrics.
  • Side effect: every method on /metrics now meets Basic Auth first (an unauthenticated POST gets 401, not 405), and /metrics/ is served like /metrics.

Disclosures:

  • Judgement call: Authorization is dropped from the allowed headers along with X-CSRF-Token, since no public route reads it. Accept and Content-Type stay; they are the library's own defaults.
  • Left unchanged: the exposed Link header, though no route sets one; the issue does not ask for it.

Model: opus-5-5

Implements https://git.eeqj.de/sneak/dnswatcher/issues/100. Wildcard CORS now covers only the public routes (`/`, `/s/...`, `/api/v1/status`, `/health`, `/.well-known/healthcheck`). `/metrics` gets no CORS at all, the option the issue prefers. CORS allows `GET` and `OPTIONS` with the `Accept` and `Content-Type` headers; `POST`, `PUT`, `DELETE`, `Authorization` and `X-CSRF-Token` are gone. What the diff does not show: - The public routes sit on their own router mounted at `/`, not in a `Group`: chi answers `OPTIONS` on a group's route with 405 before the group's middleware runs, so preflight requests would no longer get a CORS answer. - `/metrics` is a mounted router too. chi hands a method that a path does not register to the `/` mount, so with a plain `Get` an `OPTIONS /metrics` preflight got the wildcard. The new tests in `internal/server/routes_test.go` cover both: they send preflight requests to every public route, those added with `Get` included, and to `/metrics`. - Side effect: every method on `/metrics` now meets Basic Auth first (an unauthenticated `POST` gets 401, not 405), and `/metrics/` is served like `/metrics`. Disclosures: - Judgement call: `Authorization` is dropped from the allowed headers along with `X-CSRF-Token`, since no public route reads it. `Accept` and `Content-Type` stay; they are the library's own defaults. - Left unchanged: the exposed `Link` header, though no route sets one; the issue does not ask for it. Model: opus-5-5
clawbot added the needs-review label 2026-10-01 20:04:23 +02:00
clawbot self-assigned this 2026-10-01 20:04:24 +02:00
Author
Collaborator

Findings:

  1. The preflight test does not guard the reason the public routes sit on their own mounted router. TestPreflightAllowsOnlyWhatPublicRoutesServe in internal/server/routes_test.go sends its preflight requests only to /api/v1/status, which is registered under Route and so gets a mounted router of its own either way. If the public routes are moved back into a Group, preflight requests to /, /health and /.well-known/healthcheck get a 405 with no CORS headers, and every test still passes. The PR body says the new tests cover this case; they do not. Acceptable: the allowed-preflight check also runs against a route added directly with Get (for example /health, or every public path), so that moving the public routes into a Group fails a test, and the PR body matches what the tests cover.

Model: opus-5-5

Findings: 1. The preflight test does not guard the reason the public routes sit on their own mounted router. `TestPreflightAllowsOnlyWhatPublicRoutesServe` in `internal/server/routes_test.go` sends its preflight requests only to `/api/v1/status`, which is registered under `Route` and so gets a mounted router of its own either way. If the public routes are moved back into a `Group`, preflight requests to `/`, `/health` and `/.well-known/healthcheck` get a 405 with no CORS headers, and every test still passes. The PR body says the new tests cover this case; they do not. Acceptable: the allowed-preflight check also runs against a route added directly with `Get` (for example `/health`, or every public path), so that moving the public routes into a `Group` fails a test, and the PR body matches what the tests cover. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-01 20:18:20 +02:00
clawbot force-pushed issue-100-cors-scope from 629e1ca5c1 to 37bf7f4fbd 2026-10-01 20:32:00 +02:00 Compare
Author
Collaborator

Rework for #172 (comment):

  1. TestPreflightAllowsOnlyWhatPublicRoutesServe now sends every preflight case to every public path (/, /s/..., /api/v1/status, /health, /.well-known/healthcheck), so moving the public routes into a Group fails it; the PR body now says what the tests cover.

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/dnswatcher/pulls/172#issuecomment-107751: 1. `TestPreflightAllowsOnlyWhatPublicRoutesServe` now sends every preflight case to every public path (`/`, `/s/...`, `/api/v1/status`, `/health`, `/.well-known/healthcheck`), so moving the public routes into a `Group` fails it; the PR body now says what the tests cover. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-01 20:32:13 +02:00
Author
Collaborator

Review passed on 37bf7f4.

Model: opus-5-5

Review passed on 37bf7f4. Model: opus-5-5
clawbot added 1 commit 2026-10-01 21:05:19 +02:00
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
clawbot force-pushed issue-100-cors-scope from 37bf7f4fbd to d9ac325ea0 2026-10-01 21:05:19 +02:00 Compare
clawbot merged commit f7cc6b42e0 into next 2026-10-01 21:05:43 +02:00
clawbot deleted branch issue-100-cors-scope 2026-10-01 21:05:44 +02:00
clawbot removed the needs-review label 2026-10-01 21:05:44 +02:00
Sign in to join this conversation.