GET Method CSRF Protection Plugin
Use GetMethodCsrfProtectionHandlerPlugin to make the safe GET method as secure as POST by rejecting navigations that may carry SameSite=Lax cookies from another site.
How It Works
Cross-site, browsers withhold explicitly marked SameSite=Lax cookies from unsafe methods such as POST, but still attach them to safe methods like GET on top-level navigations, per RFC 6265bis. The plugin closes that gap by rejecting exactly those navigations with a 403 before routing:
| Request from another site | Sends SameSite=Lax cookies |
|
|---|---|---|
link click, redirect, window.open, GET form |
yes | rejected |
| address bar, bookmark, link from an email or native app | yes | rejected |
fetch, XMLHttpRequest |
no | allowed, CORS governs the response |
<img>, <script>, media, prefetch |
no | allowed |
<iframe>, <embed>, <object>, which are not top-level |
no | allowed |
any method other than GET |
no | ignored |
The verdict comes from Fetch Metadata: a GET request is rejected when Sec-Fetch-Site reports cross-site or none on a top-level navigation (Sec-Fetch-Mode: navigate targeting Sec-Fetch-Dest: document). A guarded request stripped of either header is rejected. Only GET needs guarding, since navigations use no method besides GET and the unsafe POST. The result resembles upgrading your cookies to SameSite=Strict for safe methods, and also covers links opened from outside the browser, where browsers attach even Strict cookies.
Cookie-less cross-site requests pass, which makes the plugin the natural safeguard for enabling the GET method. Requests from your own site always pass, including sibling subdomains, since the SameSite cookie model makes the site the trust boundary. Host untrusted content on a separate site, not a subdomain.
Setup
import { class GetMethodCsrfProtectionHandlerPlugin<T extends Context>Adds Cross-Site Request Forgery (CSRF) protection that makes the safe `GET` method as
secure as unsafe ones such as `POST`. It rejects `GET` requests arriving as top-level
navigations initiated cross-site or from outside the browser, the only context where
another site can make a browser attach `SameSite=Lax` cookies to a safe-method request.GetMethodCsrfProtectionHandlerPlugin } from '@orpc/server/plugins'
import { const RPC_DEFAULT_ALLOW_METHODS: readonly StandardMethod[]Methods that can invoke procedures by default. Browsers cannot trigger them
cross-site without a CORS preflight or an HTML form, unlike `GET`, which a plain
`<a>` click or redirect can trigger with `SameSite=Lax` cookies attached. Other
methods (`HEAD`, `OPTIONS`, `QUERY`, ...) have safe semantics that should not
invoke a procedure that can modify data.RPC_DEFAULT_ALLOW_METHODS } from '@orpc/server/standard'
const const handler: RPCHandler<{
headers?: IncomingHttpHeaders;
} & object>
handler = new new RPCHandler<{
headers?: IncomingHttpHeaders;
} & object>(router: Router<{
headers?: IncomingHttpHeaders;
} & object>, options?: NoInfer<RPCHandlerOptions<{
headers?: IncomingHttpHeaders;
} & object>>): RPCHandler<{
headers?: IncomingHttpHeaders;
} & object>
Serves an oRPC router over the RPC protocol using the Fetch API
(Request/Response), supported by modern runtimes like Deno, Bun,
Cloudflare Workers, and browsers.RPCHandler(const router: {
planet: {
list: ImplementedProcedure<{
headers?: IncomingHttpHeaders;
} & object, object, ZodObject<{
limit: ZodOptional<ZodNumber>;
cursor: ZodDefault<ZodNumber>;
}, $strip>, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ImplementedProcedure<{
headers?: IncomingHttpHeaders;
} & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<...>, object>;
create: ImplementedProcedure<...>;
};
}
router, {
RPCMatcherOptions.allowMethods?: readonly StandardMethod[] | ((method: StandardMethod, procedure: AnyProcedure, path: string[]) => boolean) | undefinedRestricts which HTTP methods can invoke procedures, either with a list of allowed
methods or decided per request via a function. Requests using a disallowed method
are treated as unmatched. `GET` is excluded by default because it is exposed to
Cross-Site Request Forgery (CSRF) attacks.allowMethods: ['GET', ...const RPC_DEFAULT_ALLOW_METHODS: readonly StandardMethod[]Methods that can invoke procedures by default. Browsers cannot trigger them
cross-site without a CORS preflight or an HTML form, unlike `GET`, which a plain
`<a>` click or redirect can trigger with `SameSite=Lax` cookies attached. Other
methods (`HEAD`, `OPTIONS`, `QUERY`, ...) have safe semantics that should not
invoke a procedure that can modify data.RPC_DEFAULT_ALLOW_METHODS],
FetchHandlerOptions<{ headers?: IncomingHttpHeaders; } & object>.plugins?: FetchHandlerPlugin<{
headers?: IncomingHttpHeaders;
} & object>[] | undefined
plugins: [
new new GetMethodCsrfProtectionHandlerPlugin<{
headers?: IncomingHttpHeaders;
} & object>(): GetMethodCsrfProtectionHandlerPlugin<{
headers?: IncomingHttpHeaders;
} & object>
Adds Cross-Site Request Forgery (CSRF) protection that makes the safe `GET` method as
secure as unsafe ones such as `POST`. It rejects `GET` requests arriving as top-level
navigations initiated cross-site or from outside the browser, the only context where
another site can make a browser attach `SameSite=Lax` cookies to a safe-method request.GetMethodCsrfProtectionHandlerPlugin(),
],
})
Cookie Requirements
Mark authentication cookies SameSite=Lax or SameSite=Strict explicitly. Do not rely on browser defaults: only Chrome treats unmarked cookies as Lax, while Firefox and Safari treat them like SameSite=None, and Chrome still sends fresh unmarked cookies on cross-site POST for two minutes.
With SameSite=None or unmarked cookies, cross-site requests the plugin allows, such as fetch and <img>, can still carry them. In that case, fix the cookie attribute or add a synchronizer token.
Limitations
- Fetch Metadata is Baseline widely available, supported by every major browser since Safari 16.4 in March 2023. Older browsers and header-stripping proxies pass through unchecked.
- Browsers send Fetch Metadata only to trustworthy URLs: HTTPS and
localhost. Over plain HTTP the headers are absent while cookies are not, so every request passes, andlocalhostqualifying hides this in development. - Users cannot open guarded procedures by typing the URL, following a bookmark, or clicking a link, since those navigations look identical to a forged one. Test with
curlor a same-origin page instead. - Prefer
SameSite=Strictor a synchronizer token for high-value requests.
Learn More
Learn more about the attack this plugin prevents on MDN and in the OWASP CSRF Prevention Cheat Sheet. For implementation details, see the source code.