Skip to content

CorsLayer::permissive() is applied to the whole router, including /api/auth #710

Description

@forkwright

Finding

crates/paroche/src/lib.rs:98 applies CorsLayer::permissive() to the router as a whole. That same
router nests the authentication routes at crates/paroche/src/lib.rs:33:

.nest("/api/auth", routes::user::auth_routes())
...
.layer(CorsLayer::permissive())

CorsLayer::permissive() allows any origin, any method, and any header. Applied at the router level it
covers every nested route, so the authentication surface is included rather than exempted.

Evidence

  • crates/paroche/src/lib.rs:98.layer(CorsLayer::permissive()), router-level.
  • crates/paroche/src/lib.rs:33.nest("/api/auth", routes::user::auth_routes()) on the same router.
  • Auth endpoints confirmed live by the integration tests: crates/archon/tests/config_reload_e2e.rs:145
    posts to /api/auth/login, :308 posts to /api/auth/refresh.

Why this matters

Permissive CORS on an authentication surface means any origin can issue cross-origin requests to login
and token-refresh endpoints and read the responses. The severity depends on how the resulting session
is carried: if credentials ride in a cookie, permissive() is the precondition for cross-site request
forgery against those endpoints; if a bearer token is returned in the body, any origin the user visits
can read a token minted for this service.

The exposure is not theoretical for this repo specifically. harmonia is a media server — the class of
software that is routinely reachable from a browser on the same network, and often deliberately exposed
beyond it. That is materially different from a loopback-bound service, where the same code would be
low-risk.

permissive() is the correct default while a frontend is being developed against a moving API, which
is very likely how it got here. It is the wrong default once auth routes are on the same router.

Desired correction

Do not apply a blanket permissive layer over a router carrying authentication. Either:

  • Replace permissive() with an explicit origin allow-list, method set, and header set, driven by
    configuration rather than hardcoded, so a deployment declares who may call it; or
  • Scope CORS to the routes that genuinely need cross-origin access and leave /api/auth off it.

If credentials are cookie-borne, note that CorsLayer::permissive() and allow_credentials(true) are
mutually exclusive in tower_http by design — the wildcard origin is rejected with credentials — so
confirm which mode the client actually relies on before changing it, or the fix will surface as a
client breakage rather than as the security change it is.

Done when: a cross-origin request from an unlisted origin to /api/auth/login is rejected by the
CORS layer, and a test pins that rejection so the permissive default cannot return unnoticed.

Found during a fleet-wide security sweep scoped to kanon; this instance is in harmonia and is the
highest-severity item the sweep produced, because unlike the kanon surfaces it sits on a service
designed to be reachable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecuritySecurity findings and fixes

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions