Registered globally at internal/server/routes.go:25 (s.router.Use(s.mw.CORS())).
Two distinct problems:
1. Wildcard CORS is applied to the authenticated route.REPO_POLICIES.md says wildcard Access-Control-Allow-Origin "is acceptable only for public, unauthenticated read-only APIs", and that "authenticated endpoints must restrict Access-Control-Allow-Origin to an explicit allowlist of known origins." /metrics is Basic-Auth protected (internal/server/routes.go:54-63, MetricsAuth() at internal/middleware/middleware.go:189-205) yet inherits the global wildcard, and AllowedHeaders explicitly includes Authorization. Wildcard CORS is genuinely fine for /, /api/v1/status, and the healthchecks — those are exactly the "public, unauthenticated, read-only" case the policy carves out. It is not fine for /metrics.
2. The advertised method set is fiction. Every route in internal/server/routes.go is registered with Get(). There is not a single POST, PUT, or DELETE handler anywhere in the service, and no handler reads a request body at all (grep for .Body, NewDecoder, ReadAll, MaxBytesReader across internal/handlers, internal/server, internal/middleware returns nothing). Advertising POST, PUT, DELETE — and X-CSRF-Token, when there are no forms and no state-mutating endpoints — is over-broad configuration that misrepresents the service's surface.
Definition of done
AllowedMethods is reduced to the methods the service actually serves: GET and OPTIONS.
X-CSRF-Token is removed from AllowedHeaders (no forms, no state-mutating endpoints exist). Keep only headers a cross-origin reader genuinely needs.
Wildcard CORS no longer applies to /metrics. Either scope the CORS middleware to the public route group only and leave /metrics without CORS entirely (preferred — a Prometheus scraper is not a browser and needs no CORS), or give the /metrics group its own restrictive CORS configuration. Whichever is chosen, the public routes (/, /s/..., /api/v1/status, /health, /.well-known/healthcheck) keep their wildcard, which the policy permits.
Tests in internal/middleware or internal/server assert: a cross-origin GET against a public route still gets Access-Control-Allow-Origin: *; a request against /metrics does not get a wildcard Access-Control-Allow-Origin; and the advertised Access-Control-Allow-Methods no longer contains POST, PUT, or DELETE.
make check is green, and TODO.md is updated in the same commit as the work.
The finishing commit's title must end with (closes #N) referencing this issue.
Out of scope
Security response headers (#98), http.Server timeouts (#99), and rate limiting are tracked separately — do not fold them into this PR.
The CORS middleware is registered globally and is both too broad in scope and inaccurate about what the service supports.
## Current state (audited against `origin/main`, commit `9347a28`)
`internal/middleware/middleware.go:173-187`:
```go
return cors.Handler(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: false,
MaxAge: corsMaxAge,
})
```
Registered globally at `internal/server/routes.go:25` (`s.router.Use(s.mw.CORS())`).
Two distinct problems:
**1. Wildcard CORS is applied to the authenticated route.** `REPO_POLICIES.md` says wildcard `Access-Control-Allow-Origin` "is acceptable only for public, unauthenticated read-only APIs", and that "authenticated endpoints must restrict `Access-Control-Allow-Origin` to an explicit allowlist of known origins." `/metrics` is Basic-Auth protected (`internal/server/routes.go:54-63`, `MetricsAuth()` at `internal/middleware/middleware.go:189-205`) yet inherits the global wildcard, and `AllowedHeaders` explicitly includes `Authorization`. Wildcard CORS is genuinely fine for `/`, `/api/v1/status`, and the healthchecks — those are exactly the "public, unauthenticated, read-only" case the policy carves out. It is not fine for `/metrics`.
**2. The advertised method set is fiction.** Every route in `internal/server/routes.go` is registered with `Get()`. There is not a single `POST`, `PUT`, or `DELETE` handler anywhere in the service, and no handler reads a request body at all (grep for `.Body`, `NewDecoder`, `ReadAll`, `MaxBytesReader` across `internal/handlers`, `internal/server`, `internal/middleware` returns nothing). Advertising `POST, PUT, DELETE` — and `X-CSRF-Token`, when there are no forms and no state-mutating endpoints — is over-broad configuration that misrepresents the service's surface.
## Definition of done
1. `AllowedMethods` is reduced to the methods the service actually serves: `GET` and `OPTIONS`.
2. `X-CSRF-Token` is removed from `AllowedHeaders` (no forms, no state-mutating endpoints exist). Keep only headers a cross-origin reader genuinely needs.
3. Wildcard CORS no longer applies to `/metrics`. Either scope the CORS middleware to the public route group only and leave `/metrics` without CORS entirely (preferred — a Prometheus scraper is not a browser and needs no CORS), or give the `/metrics` group its own restrictive CORS configuration. Whichever is chosen, the public routes (`/`, `/s/...`, `/api/v1/status`, `/health`, `/.well-known/healthcheck`) keep their wildcard, which the policy permits.
4. Tests in `internal/middleware` or `internal/server` assert: a cross-origin `GET` against a public route still gets `Access-Control-Allow-Origin: *`; a request against `/metrics` does **not** get a wildcard `Access-Control-Allow-Origin`; and the advertised `Access-Control-Allow-Methods` no longer contains `POST`, `PUT`, or `DELETE`.
5. `make check` is green, and `TODO.md` is updated in the same commit as the work.
The finishing commit's title must end with ` (closes #N)` referencing this issue.
## Out of scope
Security response headers (#98), `http.Server` timeouts (#99), and rate limiting are tracked separately — do not fold them into this PR.
clawbot
added this to the 1.0 milestone 2026-08-09 03:36:27 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The CORS middleware is registered globally and is both too broad in scope and inaccurate about what the service supports.
Current state (audited against
origin/main, commit9347a28)internal/middleware/middleware.go:173-187:Registered globally at
internal/server/routes.go:25(s.router.Use(s.mw.CORS())).Two distinct problems:
1. Wildcard CORS is applied to the authenticated route.
REPO_POLICIES.mdsays wildcardAccess-Control-Allow-Origin"is acceptable only for public, unauthenticated read-only APIs", and that "authenticated endpoints must restrictAccess-Control-Allow-Originto an explicit allowlist of known origins."/metricsis Basic-Auth protected (internal/server/routes.go:54-63,MetricsAuth()atinternal/middleware/middleware.go:189-205) yet inherits the global wildcard, andAllowedHeadersexplicitly includesAuthorization. Wildcard CORS is genuinely fine for/,/api/v1/status, and the healthchecks — those are exactly the "public, unauthenticated, read-only" case the policy carves out. It is not fine for/metrics.2. The advertised method set is fiction. Every route in
internal/server/routes.gois registered withGet(). There is not a singlePOST,PUT, orDELETEhandler anywhere in the service, and no handler reads a request body at all (grep for.Body,NewDecoder,ReadAll,MaxBytesReaderacrossinternal/handlers,internal/server,internal/middlewarereturns nothing). AdvertisingPOST, PUT, DELETE— andX-CSRF-Token, when there are no forms and no state-mutating endpoints — is over-broad configuration that misrepresents the service's surface.Definition of done
AllowedMethodsis reduced to the methods the service actually serves:GETandOPTIONS.X-CSRF-Tokenis removed fromAllowedHeaders(no forms, no state-mutating endpoints exist). Keep only headers a cross-origin reader genuinely needs./metrics. Either scope the CORS middleware to the public route group only and leave/metricswithout CORS entirely (preferred — a Prometheus scraper is not a browser and needs no CORS), or give the/metricsgroup its own restrictive CORS configuration. Whichever is chosen, the public routes (/,/s/...,/api/v1/status,/health,/.well-known/healthcheck) keep their wildcard, which the policy permits.internal/middlewareorinternal/serverassert: a cross-originGETagainst a public route still getsAccess-Control-Allow-Origin: *; a request against/metricsdoes not get a wildcardAccess-Control-Allow-Origin; and the advertisedAccess-Control-Allow-Methodsno longer containsPOST,PUT, orDELETE.make checkis green, andTODO.mdis updated in the same commit as the work.The finishing commit's title must end with
(closes #N)referencing this issue.Out of scope
Security response headers (#98),
http.Servertimeouts (#99), and rate limiting are tracked separately — do not fold them into this PR.clawbot referenced this issue2026-08-09 07:42:27 +02:00
clawbot referenced this issue2026-09-03 19:07:02 +02:00