Skip to content

feat(server): replace simple CSRF protection with GET method CSRF protection - #1846

Merged
dinwwwh merged 3 commits into
middleapi:mainfrom
dinwwwh:claude/safe-get-plugin-samesite-549e31
Aug 12, 2026
Merged

feat(server): replace simple CSRF protection with GET method CSRF protection#1846
dinwwwh merged 3 commits into
middleapi:mainfrom
dinwwwh:claude/safe-get-plugin-samesite-549e31

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Aug 12, 2026

Copy link
Copy Markdown
Member

Adds GetMethodCsrfProtectionHandlerPlugin, a zero-config safeguard that makes enabling GET on handlers safe with SameSite=Lax cookie authentication, and removes SimpleCsrfProtectionHandlerPlugin entirely. The new plugin rejects GET requests arriving as cross-site or browser-initiated top-level navigations, the only context where another site can make a browser attach explicitly marked SameSite=Lax cookies to a safe-method request. Cookie-less cross-site requests keep working, so public APIs need no origin allowlist.

Behavior

  • GET requests get a 403 before routing when Sec-Fetch-Site reports cross-site or none on a top-level navigation (Sec-Fetch-Mode: navigate targeting Sec-Fetch-Dest: document); a request stripped of either header is rejected rather than passed.
  • Only GET is guarded: navigations can use no method besides GET and the unsafe POST per the HTML spec, so HEAD, QUERY, and every other method pass untouched.
  • Same-origin and same-site requests always pass: the site is the SameSite trust boundary, so sibling-subdomain fetches and dev setups need no configuration.
  • Cross-site fetch, <img>, <iframe>, and other non-top-level contexts pass, since browsers never attach Lax cookies to them.
  • Non-browser clients and plain-HTTP deployments pass unchanged (no Fetch Metadata), and batch requests are judged before splitting, so forged sub-request headers cannot overturn the verdict.

Breaking changes

  • SimpleCsrfProtectionHandlerPlugin no longer exists; imports fail at compile time. The replacement for cookie-based apps is GetMethodCsrfProtectionHandlerPlugin with cookies explicitly marked SameSite=Lax or Strict; apps that relied on rejecting all cross-site traffic need their own safeguard, such as a synchronizer token.
  • The v1 migration guide now maps the old CSRF plugin pair to the new plugin.

Docs

  • New page documents which request contexts send SameSite=Lax cookies (links, address bar, email links, <img>, <iframe>, fetch, forms) and the resulting verdicts, plus cookie requirements: only Chrome defaults unmarked cookies to Lax, so the attribute must be set explicitly.
  • The RPC handler page's "Enabling the GET Method" section now recommends the new plugin.

Testing

  • Tests cover every navigation vector, cookie-less context, ignored methods, stripped and repeated headers, and batch forgery, with 100% statement and branch coverage on the plugin.
  • Full suite green: root vitest, pnpm type:check, and pnpm docs:validate (JSDoc backlinks + strict link check) all pass.

…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.
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orpc Ready Ready Preview Aug 12, 2026 2:31am

@pkg-pr-new

pkg-pr-new Bot commented Aug 12, 2026

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@1846

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@1846

@orpc/bun

npm i https://pkg.pr.new/@orpc/bun@1846

@orpc/client

npm i https://pkg.pr.new/@orpc/client@1846

@orpc/cloudflare

npm i https://pkg.pr.new/@orpc/cloudflare@1846

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@1846

@orpc/experimental-effect

npm i https://pkg.pr.new/@orpc/experimental-effect@1846

@orpc/evlog

npm i https://pkg.pr.new/@orpc/evlog@1846

@orpc/hibernation

npm i https://pkg.pr.new/@orpc/hibernation@1846

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@1846

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@1846

@orpc/next

npm i https://pkg.pr.new/@orpc/next@1846

@orpc/node

npm i https://pkg.pr.new/@orpc/node@1846

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@1846

@orpc/opentelemetry

npm i https://pkg.pr.new/@orpc/opentelemetry@1846

@orpc/pinia-colada

npm i https://pkg.pr.new/@orpc/pinia-colada@1846

@orpc/pino

npm i https://pkg.pr.new/@orpc/pino@1846

@orpc/publisher

npm i https://pkg.pr.new/@orpc/publisher@1846

@orpc/ratelimit

npm i https://pkg.pr.new/@orpc/ratelimit@1846

@orpc/server

npm i https://pkg.pr.new/@orpc/server@1846

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@1846

@orpc/swr

npm i https://pkg.pr.new/@orpc/swr@1846

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@1846

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@1846

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@1846

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@1846

commit: bec307f

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 30 untouched benchmarks


Comparing dinwwwh:claude/safe-get-plugin-samesite-549e31 (bec307f) with main (18576d4)

Open in CodSpeed

…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.
@dinwwwh dinwwwh changed the title feat(server): replace simple CSRF protection with safe-method CSRF protection feat(server): replace simple CSRF protection with GET method CSRF protection Aug 12, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • New SafeMethodCsrfProtectionHandlerPlugin (packages/server/src/plugins/safe-method-csrf-protection.ts:20) — a zero-config plugin that blocks GET/HEAD requests which are cross-site or none-site top-level navigations (Sec-Fetch-Site ∈ {cross-site, none, unrecognized}, Sec-Fetch-Mode: navigate, Sec-Fetch-Dest: document), returning 403 before routing, with a fail-safe that also blocks a request stripped of either sec-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 in plugins/index.ts swapped; index.test.ts updated.
  • 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 against CompositeStandardHandlerPlugin/sortPlugins ordering and the RPCHandler + BatchHandlerPlugin integration tests).
  • Tests — 33 tests covering every navigation vector, cookie-less context, stripped/repeated headers, and batch forgery, plus real-Request integration tests against a GET-enabled RPCHandler.
  • Docs — new safe-method-csrf-protection.mdx with a context→verdict table, cookie requirements, and honest limitations (plain HTTP/header-stripping proxies pass unchecked, same-site subdomain trust boundary, top-level navigation usability tradeoff); rpc/handler.mdx GET section and migrations/from-v1.mdx remapped 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.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 SafeMethodCsrfProtectionHandlerPluginGetMethodCsrfProtectionHandlerPlugin across 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 now request.method !== 'GET' instead of guarding GET/HEAD, with a clarifying comment. Rationale is correct: the HTML spec limits navigations to GET and POST (forms likewise), so a cross-site top-level navigation can never be HEAD and therefore never carries a SameSite=Lax cookie cross-site — dropping HEAD loses no protection.
  • Tests updated to assert HEAD, POST, QUERY pass and only GET is guarded (added explicit HEAD to 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.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@dinwwwh
dinwwwh merged commit 9fab60a into middleapi:main Aug 12, 2026
11 checks passed
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