Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

Safe Method CSRF Protection Plugin

Use SafeMethodCsrfProtectionHandlerPlugin to make safe methods like GET as secure as POST by rejecting requests 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
POST and other unsafe methods no ignored

The verdict comes from Fetch Metadata: a GET or HEAD 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. 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 SafeMethodCsrfProtectionHandlerPlugin<T extends Context>
Adds Cross-Site Request Forgery (CSRF) protection that makes safe HTTP methods as secure as unsafe ones such as `POST`. It rejects `GET` and `HEAD` requests arriving as top-level navigations initiated cross-site or from outside the browser, the only contexts where another site can make a browser attach `SameSite=Lax` cookies to a safe-method request.
@remarks**Note**: Requests browsers send without `SameSite=Lax` cookies, such as cross-site `fetch` and `<img>`, pass through, so procedures stay reachable from other sites. This safeguard requires authentication cookies explicitly marked `SameSite=Lax` or `SameSite=Strict`, since browsers may attach other cookies to the requests that pass.@see{@link https://orpc.dev/docs/plugins/safe-method-csrf-protection Safe Method CSRF Protection Plugin}
SafeMethodCsrfProtectionHandlerPlugin
} 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.
@see{@link https://orpc.dev/docs/rpc/handler#supported-http-methods RPC Handler - Supported HTTP Methods}
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.
@see{@link https://orpc.dev/docs/adapters/fetch-api Fetch API Adapter}
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) | undefined
Restricts 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.
@defaultRPC_DEFAULT_ALLOW_METHODS (['POST', 'PUT', 'PATCH', 'DELETE'])@see{@link https://orpc.dev/docs/rpc/handler#supported-http-methods RPC Handler - Supported HTTP Methods}
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.
@see{@link https://orpc.dev/docs/rpc/handler#supported-http-methods RPC Handler - Supported HTTP Methods}
RPC_DEFAULT_ALLOW_METHODS
],
FetchHandlerOptions<{ headers?: IncomingHttpHeaders; } & object>.plugins?: FetchHandlerPlugin<{
    headers?: IncomingHttpHeaders;
} & object>[] | undefined
plugins
: [
new
new SafeMethodCsrfProtectionHandlerPlugin<{
    headers?: IncomingHttpHeaders;
} & object>(): SafeMethodCsrfProtectionHandlerPlugin<{
    headers?: IncomingHttpHeaders;
} & object>
Adds Cross-Site Request Forgery (CSRF) protection that makes safe HTTP methods as secure as unsafe ones such as `POST`. It rejects `GET` and `HEAD` requests arriving as top-level navigations initiated cross-site or from outside the browser, the only contexts where another site can make a browser attach `SameSite=Lax` cookies to a safe-method request.
@remarks**Note**: Requests browsers send without `SameSite=Lax` cookies, such as cross-site `fetch` and `<img>`, pass through, so procedures stay reachable from other sites. This safeguard requires authentication cookies explicitly marked `SameSite=Lax` or `SameSite=Strict`, since browsers may attach other cookies to the requests that pass.@see{@link https://orpc.dev/docs/plugins/safe-method-csrf-protection Safe Method CSRF Protection Plugin}
SafeMethodCsrfProtectionHandlerPlugin
(),
], })

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, and localhost qualifying 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 curl or a same-origin page instead.
  • Prefer SameSite=Strict or 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.

Last updated on August 12, 2026

Was this page helpful?