All guidesMore providers: databases, auth, hosting, email and payments
On this page

SvelteKit error

SvelteKit

Cross-site POST form submissions are forbidden

By Ohad Krispin, founder of VibeRaven · LinkedIn

"Cross-site POST form submissions are forbidden" is the 403 SvelteKit returns in production when a POST, PUT, PATCH or DELETE has a form content type and its Origin header is missing or differs from the origin of the request URL. The check is off in local development, so it first shows up after a deploy. When an outside service posts a form to your route, add its full origin to csrf.trustedOrigins. When your own form fails, the origin SvelteKit sees is not the one the browser used, so fix the origin: behind your own proxy on adapter-node set ORIGIN, or paths.origin in SvelteKit 3. Reproduced with @sveltejs/kit 2.70.3 on 2026-10-31.

What SvelteKit compares, and what the 403 looks like

In SvelteKit 2 the check lives in respond.js. It applies only when the build is not a development build, when csrf.checkOrigin is not off, when the Content-Type is application/x-www-form-urlencoded, multipart/form-data or text/plain, and the method is POST, PUT, PATCH or DELETE. The request is refused when the Origin header is not the URL origin and is not in csrf.trustedOrigins. The method in the message is the real method. The body is plain text, or JSON when the request sends Accept: application/json. Against a local production preview, the result was:

POST /  Origin: https://my-app.example.com  Content-Type: application/x-www-form-urlencoded
HTTP/1.1 403 Forbidden
Cross-site POST form submissions are forbidden

same request with Accept: application/json
HTTP/1.1 403 Forbidden
{"message":"Cross-site POST form submissions are forbidden"}

An outside service posts a form to my route: how do I allow it?

List the sender's origin in csrf.trustedOrigins, with the protocol and no path. The SvelteKit docs describe it as the origins allowed to make cross-origin form submissions, for services such as payment gateways or authentication providers, and warn to add only origins you completely trust. In the 2026-10-31 run, with this setting, a form POST from the listed origin got 200 and a POST from another origin got 403. A request with no Origin header at all stayed 403, because the check refuses a missing Origin even when the list has entries. If you control the sender, post JSON to a +server.js endpoint instead: JSON is not a form content type.

// svelte.config.js (SvelteKit 2)
export default {
  kit: {
    csrf: { trustedOrigins: ['https://pay.example.com'] }
  }
};

My own form fails behind a proxy or on a custom domain

Then the origin SvelteKit derives from the request URL is not the one the browser sent in Origin, for example http instead of https or an internal host. The adapter-node docs say that when it cannot work out the deployment URL, form actions can fail with this exact error; ORIGIN tells SvelteKit where the app is served, and PROTOCOL_HEADER and HOST_HEADER are for use only behind a trusted proxy. SvelteKit 3 adds paths.origin, which is used as the trusted origin for the check. adapter-vercel 5.10.3 passes the request to SvelteKit unchanged, apart from restoring the path on ISR requests, so it sets no origin of its own.

Checklist

How to fix it, then check the fix

  1. 01
    Reproduce it against your own deployment with a form POST from a made-up origin. It should answer 403 with the message:
    curl -i -X POST https://your-domain.example/your-route -H "Origin: https://other.example" -H "Content-Type: application/x-www-form-urlencoded" -d "a=1"
  2. 02Find out who sends the failing request. If it is a form on your own site, compare the address bar with the site you meant: www and the bare domain, a preview URL and the production domain are different origins.
  3. 03If an outside service sends it, add exactly its origin, protocol included, to csrf.trustedOrigins. Use '*' only as a last resort: the SvelteKit docs say it is generally not recommended, and it turns the check off for every origin.
  4. 04If the sender cannot send an Origin header, change it to post JSON, or accept that the route needs trustedOrigins set to '*'. A missing Origin is refused whenever the list does not contain '*'.
  5. 05If the app runs on adapter-node behind your own proxy, set ORIGIN, or paths.origin in SvelteKit 3. Set PROTOCOL_HEADER and HOST_HEADER only if the proxy is trusted and overwrites those headers.
  6. 06Redeploy, then repeat the curl with the origin you trusted. It should no longer answer 403 with this message.

Related

Sources

Official docs behind this checklist

FAQ

FAQ for builders before launch

Should I set csrf.checkOrigin to false?

Not in new code. In SvelteKit 2.70.3 the option is marked deprecated in favour of trustedOrigins: ['*'], and SvelteKit 3 removed it, with the docs saying CSRF protection is always on. Both ways switch the protection off for every origin, so list the origins you trust instead.

Why did it work with npm run dev?

The SvelteKit docs say CSRF checks only apply in production, not in local development. The check in respond.js sits inside a branch that runs only when the build is not a development build.

Does a JSON webhook hit this error?

Not in SvelteKit 2. The form check applies only to form content types, and application/json is not one of them. In SvelteKit 3 the check also applies to a request with no Content-Type header, so a body-less POST with no Origin is refused there.