Skip to content

fix(coderd): restrict CSRF exemptions to exact paths and enforce JSON content type for cookie-authenticated bodies - #28067

Draft
jdomeracki-coder wants to merge 3 commits into
mainfrom
jd/sec-421-csrf-exemption-hardening
Draft

fix(coderd): restrict CSRF exemptions to exact paths and enforce JSON content type for cookie-authenticated bodies#28067
jdomeracki-coder wants to merge 3 commits into
mainfrom
jd/sec-421-csrf-exemption-hardening

Conversation

@jdomeracki-coder

Copy link
Copy Markdown
Contributor

Defense-in-depth hardening of the CSRF protections in coderd. Two independent layers:

1. coderd/httpmw: remove unanchored CSRF exemption regexes

nosurf evaluates ExemptRegexp patterns with MatchString, i.e. as unanchored substring matches against the request path, and skips both the token check and its same-origin validation on exempt paths. Patterns like derp/* therefore exempted every /api path merely containing that substring — including attacker-influenceable path segments such as usernames, organization names, and task names (all of which legally allow such substrings).

All removed regexes were redundant:

  • Agents, workspace proxies, and provisioner daemons authenticate via headers/PSK and send no session cookie, so the existing ExemptFunc no-cookie exemption already applies to them.
  • /derp and /scim are not under /api and are already exempt via the ExemptFunc prefix check; no /api/v2/scim route exists.
  • Only GET routes exist under organizations/{org}/provisionerdaemons, and GET is a nosurf safe method.
  • The dashboard sends X-CSRF-TOKEN on every request, so cookie-based browser flows on the previously exempted routes (e.g. devcontainer recreate) pass the standard CSRF checks.

Exemptions are now exact-path only: /api/v2/csp/reports (unchanged) and /api/v2/users/first (converted from regex).

2. coderd/httpapi: require application/json for cookie-authenticated JSON bodies

httpapi.Read previously decoded JSON regardless of the declared Content-Type. Browsers send cross-origin POSTs without a CORS preflight only for "simple" content types (text/plain, form encodings), so accepting JSON from such bodies would make any endpoint that slips past the CSRF middleware forgeable from a cross-site page.

Read now returns 415 Unsupported Media Type unless the request declares application/json. The check applies only to requests carrying a session cookie:

  • a browser attacker cannot set application/json without triggering a preflight, and cannot remove the victim's cookie — so this closes the vector;
  • header-token clients (CLI, agents, scripts) that omit or mis-set Content-Type are unaffected;
  • first-party clients already send application/json (codersdk client.go, dashboard axios defaults).

Compatibility notes

  • Dashboard flows on previously exempted routes are unaffected (axios sets X-CSRF-TOKEN globally).
  • Agents / workspace proxies / provisioner daemons / SCIM / DERP are unaffected (no session cookies; non-/api paths remain exempt).
  • The only behavior change for external consumers: clients that authenticate with a session cookie while sending a non-JSON Content-Type now receive 415 with a descriptive error. Cookie auth for scripts is not the documented integration path (header tokens are).

Tests

  • TestCSRFExemptList extended: pins exact-path exemptions, non-/api prefix exemptions, enforcement on paths whose segments contain formerly-exempt substrings, and the no-cookie ExemptFunc behavior relied on by agents/proxies/daemons.
  • New TestReadContentType matrix covering cookie/no-cookie × content-type combinations.

@linear-code

linear-code Bot commented Aug 12, 2026

Copy link
Copy Markdown

SEC-421

@jdomeracki-coder jdomeracki-coder self-assigned this Aug 12, 2026
nosurf matches exemption regexes with MatchString, i.e. as unanchored
substring matches against the request path, and skips both the CSRF
token check and its same-origin validation on exempt paths. Patterns
like "derp/*" therefore exempted every /api path merely containing
the substring "derp", including attacker-influenceable path segments
such as usernames, organization names, and task names.

All removed regexes were redundant:

- Agents, workspace proxies, and provisioner daemons authenticate via
  headers/PSK and send no session cookie, so the ExemptFunc no-cookie
  exemption already applies.
- /derp and /scim are not under /api and are already exempt via the
  ExemptFunc prefix check; no /api/v2/scim route exists.
- Only GET routes exist under organizations/{org}/provisionerdaemons,
  and GET is a nosurf safe method.
- The dashboard sends X-CSRF-TOKEN on every request, so cookie-based
  browser flows on the previously exempted routes pass the standard
  CSRF checks.

/api/v2/users/first is retained as an exact-path exemption, and
/api/v2/csp/reports is unchanged. Exemptions are now exact-path only.
…d JSON bodies

httpapi.Read previously decoded JSON regardless of the declared
Content-Type. Browsers send cross-origin POSTs without a CORS
preflight only for "simple" content types (text/plain, form
encodings), so accepting JSON from such bodies makes any endpoint
that slips past the CSRF middleware forgeable from a cross-site page.

Read now returns 415 Unsupported Media Type unless the request
declares application/json. The check only applies to requests
carrying a session cookie: a browser attacker cannot set
application/json without triggering a preflight and cannot remove the
victim's cookie, while header-token clients (CLI, agents, scripts)
that omit Content-Type are unaffected. First-party clients already
send application/json (codersdk and the dashboard).
@jdomeracki-coder
jdomeracki-coder force-pushed the jd/sec-421-csrf-exemption-hardening branch from d81136c to 46d7433 Compare August 12, 2026 13:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant