feat(server): replace simple CSRF protection with GET method CSRF protection - #1846
Conversation
…otection SafeMethodCsrfProtectionHandlerPlugin is a zero-config safeguard for enabling GET on handlers with SameSite=Lax cookie authentication. It rejects GET/HEAD requests arriving as cross-site or browser-initiated top-level navigations, the only contexts where another site can make a browser attach explicitly marked SameSite=Lax cookies to a safe-method request, while cookie-less cross-site requests keep working without an origin allowlist. SimpleCsrfProtectionHandlerPlugin is removed along with its docs page; the migration guide now points at the new plugin.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
More templates
@orpc/ai-sdk
@orpc/arktype
@orpc/bun
@orpc/client
@orpc/cloudflare
@orpc/contract
@orpc/experimental-effect
@orpc/evlog
@orpc/hibernation
@orpc/json-schema
@orpc/nest
@orpc/next
@orpc/node
@orpc/openapi
@orpc/opentelemetry
@orpc/pinia-colada
@orpc/pino
@orpc/publisher
@orpc/ratelimit
@orpc/server
@orpc/shared
@orpc/swr
@orpc/tanstack-query
@orpc/trpc
@orpc/valibot
@orpc/zod
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…nHandlerPlugin Navigations can only use GET or POST per the HTML spec, and POST is unsafe, so GET is the only method SameSite=Lax cookies ride cross-site. HEAD and QUERY need no guard: browsers cannot navigate with them, and their scripted forms never carry Lax cookies cross-site. The plugin name now states exactly what it guards.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- New
SafeMethodCsrfProtectionHandlerPlugin(packages/server/src/plugins/safe-method-csrf-protection.ts:20) — a zero-config plugin that blocksGET/HEADrequests which are cross-site ornone-site top-level navigations (Sec-Fetch-Site∈ {cross-site,none, unrecognized},Sec-Fetch-Mode: navigate,Sec-Fetch-Dest: document), returning403before routing, with a fail-safe that also blocks a request stripped of eithersec-fetch-mode/sec-fetch-dest. Same-origin/same-site/absent-site requests and cookie-less cross-site contexts (fetch,<img>,<iframe>, websocket, extension) pass. - Removal of
SimpleCsrfProtectionHandlerPlugin(breaking) — deleted along with its docs and tests; export inplugins/index.tsswapped;index.test.tsupdated. - Batch ordering:
after = ['~batch']+ prepended routing interceptor ensures the plugin judges the original request before the batch plugin splits it, so forged sub-request headers cannot overturn the verdict (verified againstCompositeStandardHandlerPlugin/sortPluginsordering and the RPCHandler +BatchHandlerPluginintegration tests). - Tests — 33 tests covering every navigation vector, cookie-less context, stripped/repeated headers, and batch forgery, plus real-
Requestintegration tests against a GET-enabledRPCHandler. - Docs — new
safe-method-csrf-protection.mdxwith a context→verdict table, cookie requirements, and honest limitations (plain HTTP/header-stripping proxies pass unchecked,same-sitesubdomain trust boundary, top-level navigation usability tradeoff);rpc/handler.mdxGET section andmigrations/from-v1.mdxremapped to the new plugin.
I verified the load-bearing security claim against primary sources (RFC 6265bis §5.8.3, W3C Fetch Metadata spec, MDN): a browser attaches a SameSite=Lax cookie to a cross-site request only on a safe-method request targeting a top-level traversable, and every top-level navigation reports Sec-Fetch-Mode: navigate + Sec-Fetch-Dest: document, while embedded contexts report other destinations. The plugin blocks exactly that combination. The residual gaps (browsers/proxies that strip Fetch Metadata, plain HTTP, and trusting same-site) are explicitly and accurately documented, and the design is correctly scoped as a companion to SameSite=Lax/Strict cookie auth rather than a substitute for it. No actionable issues found.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Incremental review of commit 7c8a773 (refactor(server): guard GET only and rename to GetMethodCsrfProtectionHandlerPlugin) on top of the previously-approved 369be220.
- Rename
SafeMethodCsrfProtectionHandlerPlugin→GetMethodCsrfProtectionHandlerPluginacross the plugin, its tests,plugins/index.ts(export moved up),plugins/index.test.ts, and all three doc pages — with no leftover references to the old name anywhere in the repo. - GET-only guard (
get-method-csrf-protection.ts:47): the method check is nowrequest.method !== 'GET'instead of guardingGET/HEAD, with a clarifying comment. Rationale is correct: the HTML spec limits navigations toGETandPOST(forms likewise), so a cross-site top-level navigation can never beHEADand therefore never carries aSameSite=Laxcookie cross-site — droppingHEADloses no protection. - Tests updated to assert
HEAD,POST,QUERYpass and onlyGETis guarded (added explicitHEADto the "ignored methods" cases).
The behavioral narrowing is sound and the rename accurately reflects what the plugin does. No actionable issues found in the delta.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Adds
GetMethodCsrfProtectionHandlerPlugin, a zero-config safeguard that makes enablingGETon handlers safe withSameSite=Laxcookie authentication, and removesSimpleCsrfProtectionHandlerPluginentirely. The new plugin rejectsGETrequests arriving as cross-site or browser-initiated top-level navigations, the only context where another site can make a browser attach explicitly markedSameSite=Laxcookies to a safe-method request. Cookie-less cross-site requests keep working, so public APIs need no origin allowlist.Behavior
GETrequests get a403before routing whenSec-Fetch-Sitereportscross-siteornoneon a top-level navigation (Sec-Fetch-Mode: navigatetargetingSec-Fetch-Dest: document); a request stripped of either header is rejected rather than passed.GETis guarded: navigations can use no method besidesGETand the unsafePOSTper the HTML spec, soHEAD,QUERY, and every other method pass untouched.SameSitetrust boundary, so sibling-subdomain fetches and dev setups need no configuration.fetch,<img>,<iframe>, and other non-top-level contexts pass, since browsers never attachLaxcookies to them.Breaking changes
SimpleCsrfProtectionHandlerPluginno longer exists; imports fail at compile time. The replacement for cookie-based apps isGetMethodCsrfProtectionHandlerPluginwith cookies explicitly markedSameSite=LaxorStrict; apps that relied on rejecting all cross-site traffic need their own safeguard, such as a synchronizer token.Docs
SameSite=Laxcookies (links, address bar, email links,<img>,<iframe>,fetch, forms) and the resulting verdicts, plus cookie requirements: only Chrome defaults unmarked cookies toLax, so the attribute must be set explicitly.Testing
pnpm type:check, andpnpm docs:validate(JSDoc backlinks + strict link check) all pass.