fix(graphql-server): surface CSRF rejections as JSON - #1773
Merged
Conversation
Contributor
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
Review complete. No issues found — approved ✅. This PR makes two focused changes. It adds an integration test suite covering the cookie-authenticated GraphQL CSRF flow (valid token, missing token, missing cookie, wrong token, and HTML 404 for unknown routes), and it extends the error handler's wantsJson() detection with a Content-Type check so JSON requests receive JSON error responses. The changes are small and well-scoped.
Reviewed commit: 4db3124 |
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.
Summary
GraphQL JSON requests without an explicit
Acceptheader were misclassified as HTML clients bywantsJson. CSRF middleware correctly raised a typed error and the centralized handler preserved status 403, butsendResponserendered every non-5xx HTML error with the Not Found page.This changes content negotiation to also recognize a JSON request
Content-Type, preserving the existing structured CSRF response for API clients while leaving genuine browser/HTML 404s unchanged.Closes #1681.
Before / after transcript
Requests below omit
Accept, matching the reported flow. Tokens and request IDs are redacted.mainX-CSRF-Token200 application/graphql-response+json{"data":{"__typename":"Query"}}X-CSRF-Token403 text/htmlNot Found page403 application/json{"error":{"code":"CSRF_TOKEN_INVALID","message":"CSRF token validation failed","requestId":"…"}}403 text/htmlNot Found page403 application/json{"error":{"code":"CSRF_TOKEN_MISSING","message":"CSRF token missing from cookie","requestId":"…"}}X-CSRF-Token403 text/htmlNot Found page403 application/json{"error":{"code":"CSRF_TOKEN_INVALID","message":"CSRF token validation failed","requestId":"…"}}404 text/html<title>Not Found</title>/The requested page was not foundThe JSON error never includes the expected token value.
Edge transcript:
GET /graphqltoken issuanceapplication/json{"errors":[{"message":"Method not supported, please use POST","extensions":{}}]}; setscsrf_token=<redacted>application/jsonCSRF_TOKEN_MISSINGafter the fix; previously the same 403 rendered the Not Found HTML pageapplication/graphql-response+json{"data":{"__typename":"Query"}}(unchanged)Root cause and regression proof
The issue was not CSRF middleware ordering or 404 fallthrough: the error reached
errorHandler, was categorized as 403, then the Accept-onlywantsJsoncheck selected the HTML branch and its generic 4xx renderer. Recognizing the JSON request content type fixes the representation at that content-negotiation boundary.The new five-case E2E block uses the existing
@constructive-io/graphql-server-testHTTP harness. Against unmodified production code onmain, the happy flow and genuine 404 passed, while all three CSRF rejection tests failed specifically because they receivedtext/htmlinstead of JSON (3 failed, 2 passed). With this change, all five pass.Validation:
graphql/server: 13 suites, 143 tests passedgraphql/server-test/server.integration.test.ts: 39 tests passedgraphql/server-test/scoped-routing.integration.test.ts: 7 tests passedDeliberately left alone
csrfProtectonly checks cookie presence, so it exercises the same CSRF branch, but a real sign-in/Set-Cookieleg was not locally available.Max-Age=86because of a pre-existing unit mismatch.Access-Control-Allow-Origin; adding server-side Origin validation is separate from this error-surfacing fix.GET /graphqlreturns 405 while setting the token cookie, and CSRF 403s are logged asunexpected_error; both are pre-existing and outside this focused fix.Link to Devin session: https://app.devin.ai/sessions/311a44c399bc4b10b4eafa163d9660ba
Requested by: @pyramation