Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

Simple CSRF Protection Plugin

Use SimpleCsrfProtectionHandlerPlugin to reject requests that a browser reports as initiated by another site.

How It Works

The plugin decides on Sec-Fetch-Site, the Fetch Metadata header reporting who initiated a request:

Sec-Fetch-Site initiated by
same-origin your own pages allowed
same-site another origin on your site rejected unless listed in origin or allowSameSite
cross-site another site rejected unless listed in origin
none no page, such as a link opened from an email rejected

Requests without Fetch Metadata pass through, such as those from curl, mobile apps, and other non-browser clients.

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.
  • Keep authentication cookies on SameSite=Lax at minimum, and prefer SameSite=Strict or a synchronizer token for high-value requests.

Setup

import { class SimpleCsrfProtectionHandlerPlugin<T extends Context>
Adds basic Cross-Site Request Forgery (CSRF) protection by rejecting requests a browser reports as initiated by another site, or by no page at all such as a link from an email.
@remarks**Note**: Unlike `SameSite` cookies, this also covers cross-site requests that still carry cookies, so it is the safeguard to add when enabling the `GET` method on RPC handlers.@see{@link https://orpc.dev/docs/plugins/simple-csrf-protection Simple CSRF Protection Plugin}
SimpleCsrfProtectionHandlerPlugin
} from '@orpc/server/plugins'
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
, {
FetchHandlerOptions<{ headers?: IncomingHttpHeaders; } & object>.plugins?: FetchHandlerPlugin<{
    headers?: IncomingHttpHeaders;
} & object>[] | undefined
plugins
: [
new
new SimpleCsrfProtectionHandlerPlugin<{
    headers?: IncomingHttpHeaders;
} & object>(options?: SimpleCsrfProtectionHandlerPluginOptions<{
    headers?: IncomingHttpHeaders;
} & object>): SimpleCsrfProtectionHandlerPlugin<{
    headers?: IncomingHttpHeaders;
} & object>
Adds basic Cross-Site Request Forgery (CSRF) protection by rejecting requests a browser reports as initiated by another site, or by no page at all such as a link from an email.
@remarks**Note**: Unlike `SameSite` cookies, this also covers cross-site requests that still carry cookies, so it is the safeguard to add when enabling the `GET` method on RPC handlers.@see{@link https://orpc.dev/docs/plugins/simple-csrf-protection Simple CSRF Protection Plugin}
SimpleCsrfProtectionHandlerPlugin
(),
], })

Trusting Other Origins

Only your own origin is trusted by default. Use the same allowlist you pass to the CORS Plugin:

const origin = ['https://app.example.com']

const handler = new RPCHandler(router, {
  plugins: [
    new CORSHandlerPlugin({ origin, credentials: true }),
    new SimpleCsrfProtectionHandlerPlugin({ origin }),
  ],
})

origin also accepts a single string, or a function. Every entry must be a specific origin, since there is no wildcard:

const plugin = new SimpleCsrfProtectionHandlerPlugin({
  origin: (origin, options) => {
    return origin.endsWith('.example.com') ? origin : undefined
  },
})

Set allowSameSite: true to trust your whole site at once, and only when you control every subdomain, since SameSite cookies do not distinguish them:

const plugin = new SimpleCsrfProtectionHandlerPlugin({
  allowSameSite: true,
})

Restricting Fetch Modes

A trusted site reaches your API by any means: fetch, forms, links, and <img> all pass. Restrict it to scripted requests if your app renders user-supplied URLs:

const plugin = new SimpleCsrfProtectionHandlerPlugin({
  allowModes: ['cors', 'same-origin'],
})

allowModes matches Sec-Fetch-Mode: navigate for forms and links, no-cors for <img> and <script>. Leave it unset if you rely on HTML forms, including through the Method Override Plugin. Requests without the header are never rejected.

Learn More

For implementation details, see the source code.

Last updated on August 11, 2026

Was this page helpful?