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
Draft
Conversation
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
force-pushed
the
jd/sec-421-csrf-exemption-hardening
branch
from
August 12, 2026 13:06
d81136c to
46d7433
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Defense-in-depth hardening of the CSRF protections in coderd. Two independent layers:
1.
coderd/httpmw: remove unanchored CSRF exemption regexesnosurf evaluates
ExemptRegexppatterns withMatchString, 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 likederp/*therefore exempted every/apipath 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:
ExemptFuncno-cookie exemption already applies to them./derpand/scimare not under/apiand are already exempt via theExemptFuncprefix check; no/api/v2/scimroute exists.organizations/{org}/provisionerdaemons, and GET is a nosurf safe method.X-CSRF-TOKENon 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: requireapplication/jsonfor cookie-authenticated JSON bodieshttpapi.Readpreviously decoded JSON regardless of the declaredContent-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.Readnow returns 415 Unsupported Media Type unless the request declaresapplication/json. The check applies only to requests carrying a session cookie:application/jsonwithout triggering a preflight, and cannot remove the victim's cookie — so this closes the vector;Content-Typeare unaffected;application/json(codersdkclient.go, dashboard axios defaults).Compatibility notes
X-CSRF-TOKENglobally)./apipaths remain exempt).Content-Typenow receive 415 with a descriptive error. Cookie auth for scripts is not the documented integration path (header tokens are).Tests
TestCSRFExemptListextended: pins exact-path exemptions, non-/apiprefix exemptions, enforcement on paths whose segments contain formerly-exempt substrings, and the no-cookieExemptFuncbehavior relied on by agents/proxies/daemons.TestReadContentTypematrix covering cookie/no-cookie × content-type combinations.