« Cross-site POST form submissions are forbidden » : SvelteKit a la mauvaise origine.
SvelteKit refuse un envoi de formulaire avec une erreur 403 quand l’en-tête Origin du navigateur ne correspond pas à l’origine qu’il croit servir. Après un déploiement, c’est presque toujours une variable ORIGIN erronée (ou paths.origin avec SvelteKit 3), ou un mandataire qui réécrit l’en-tête Host. Corrigez l’origine. Ne désactivez pas la vérification.
Faits vérifiés le
Ce que la vérification compare
La réponse est une erreur 403 en texte brut dont le corps est Cross-site POST form submissions are forbidden (le nom de la méthode change pour PUT, PATCH ou DELETE; une requête qui accepte du JSON reçoit {"message": …}). Elle vient de la protection CSRF de SvelteKit, qui ne s’exécute que dans les versions de production. Voilà pourquoi elle n’apparaît jamais sous vite dev.
Avec @sveltejs/adapter-node, l’origine à laquelle SvelteKit compare la requête se construit ainsi :
// What the check compares (production builds only, not vite dev)
self origin = paths.origin (SvelteKit 3) or ORIGIN (adapter-node 5)
or else <PROTOCOL_HEADER value, or "https"> :// <HOST_HEADER value, or Host>
browser sends Origin: https://shop.example.com
no match on a POST, PUT, PATCH or DELETE form -> 403
Cross-site POST form submissions are forbiddenPourquoi ça commence après un déploiement
| Ce qui s’est passé | Pourquoi les origines diffèrent | Correctif |
|---|---|---|
ORIGIN=http://localhost:3000 a suivi depuis un fichier .env † | Le navigateur envoie https://votre-domaine | Retirez ORIGIN derrière un mandataire qui conserve Host, ou donnez-lui la vraie adresse publique |
ORIGIN nomme un hôte, les visiteurs en utilisent un autre : www et le domaine nu, le sous-domaine de la plateforme et un domaine personnalisé, ou les adresses d’aperçu † | ORIGIN ne contient qu’une seule origine | Laissez-la vide pour que chaque requête utilise son propre hôte, ou redirigez tous les hôtes vers un seul |
L’appli est servie en http simple, par exemple pour un essai sur http://<ip>:3000 † | adapter-node suppose https | Réglez ORIGIN sur l’adresse http:// pour cet essai, ou servez-la en HTTPS |
Un mandataire réécrit Host : nginx envoie Host: $proxy_host (l’adresse en amont, par exemple 127.0.0.1:3000) sauf indication contraire † † | L’origine est construite à partir de l’adresse en amont | Ajoutez proxy_set_header Host $host;, ou réglez HOST_HEADER=x-forwarded-host si le mandataire définit cet en-tête |
| Vous êtes passé à SvelteKit 3 et à adapter-node 6 † | La variable ORIGIN a été retirée : elle n’a plus d’effet | Réglez paths.origin dans vite.config.js, ou fiez-vous aux en-têtes de la requête |
| Un tiers vous envoie un formulaire : une page de paiement, un fournisseur de connexion † | Cette origine est vraiment un autre site | Ajoutez-la à csrf.trustedOrigins, en origine complète |
† Les détails sur les concurrents proviennent des pages publiées par chaque fournisseur aux dates indiquées; les prix et forfaits changent, vérifiez auprès du fournisseur avant de décider. `ORIGIN=http://localhost:3000` a suivi depuis un fichier `.env` (2026-10-29) · `ORIGIN` nomme un hôte, les visiteurs en utilisent un autre : `www` et le domaine nu, le sous-domaine de la plateforme et un domaine personnalisé, ou les adresses d’aperçu (2026-10-29) · L’appli est servie en http simple, par exemple pour un essai sur `http://<ip>:3000` (2026-10-29) · Un mandataire réécrit `Host` : nginx envoie `Host: $proxy_host` (l’adresse en amont, par exemple `127.0.0.1:3000`) sauf indication contraire (2026-10-29) · Un mandataire réécrit `Host` : nginx envoie `Host: $proxy_host` (l’adresse en amont, par exemple `127.0.0.1:3000`) sauf indication contraire (2026-10-29) · Vous êtes passé à SvelteKit 3 et à adapter-node 6 (2026-10-29) · Un tiers vous envoie un formulaire : une page de paiement, un fournisseur de connexion (2026-10-29)
PROTOCOL_HEADER et HOST_HEADER
Quand aucune origine n’est configurée, adapter-node peut lire le protocole et l’hôte dans les en-têtes du mandataire : PROTOCOL_HEADER=x-forwarded-proto et HOST_HEADER=x-forwarded-host (et PORT_HEADER=x-forwarded-port si le port public n’est pas celui par défaut). La documentation de SvelteKit est claire : ne les réglez que derrière un mandataire inverse de confiance, sinon un client peut les envoyer lui-même.
Ce sont des variables lues à l’exécution, au démarrage du serveur. paths.origin, dans SvelteKit 3, est de la configuration figée à la compilation, et ne contient qu’une origine pour tous les hôtes auxquels l’appli répond.
Sur Canner : laissez l’origine vide
- Comment l’appli tourne. Canner détecte SvelteKit et lance
node ./build/index.js, le serveur d’adapter-node. Sans adapter-node, la compilation s’arrête avecSvelteKit build did not produce build/index.js — install @sveltejs/adapter-node and set it as the adapter in your SvelteKit config (svelte.config.js, or vite.config.js on SvelteKit 3).@sveltejs/adapter-vercel,-netlifyet-cloudflaresont refusés dès la détection, avec un message qui nomme adapter-node. - Ce qu’envoie le mandataire. Les requêtes passent par Caddy, qui termine le HTTPS, transmet l’en-tête
Hosttel quel et définitX-Forwarded-Proto,X-Forwarded-ForetX-Forwarded-Host. Par défaut, Caddy écarte les valeursX-Forwarded-*envoyées par un client et écrit les siennes. - Les valeurs par défaut fonctionnent donc. Sans
ORIGINnipaths.origin, adapter-node construithttps://suivi de l’hôte saisi par le visiteur. Ça correspond sur<slug>.canner.app, sur chaque domaine personnalisé et sur les adresses d’aperçu, tout à la fois. Le mandataire de mise en cache facultatif conserve aussiHost. - Ne réglez pas ORIGIN à partir de `CANNER_URL`.
CANNER_URLest l’adressecanner.appdu déploiement (ou celle de l’aperçu) : les visiteurs de votre domaine personnalisé recevraient l’erreur 403. Même chose pourpaths.origin. - Vous voulez être explicite?
PROTOCOL_HEADER=x-forwarded-protoetHOST_HEADER=x-forwarded-hostlisent les valeurs qu’écrit Caddy : le résultat est le même. Ajoutez-les dans les variables d’environnement et redéployez. Vous n’en avez pas besoin. - Version de Node. SvelteKit 3 exige Node 22.17 ou plus récent. Canner ne compare que les versions majeures : choisissez Node 24 dans les paramètres du projet pour être tranquille. Voir fixer la version de Node.js.
Ne désactivez pas la vérification
L’ancien contournement, csrf: { checkOrigin: false }, a disparu de SvelteKit 3 : la compilation échoue en indiquant que checkOrigin a été retiré au profit de csrf.trustedOrigins. Son remplaçant, trustedOrigins: ['*'], coupe la protection pour tous les sites, ce que la documentation de SvelteKit déconseille. Vos actions de formulaire acceptent alors un envoi venant de n’importe quelle page qu’un utilisateur connecté visite.
Quand un site externe doit vraiment vous envoyer un formulaire, inscrivez cette seule origine :
// vite.config.js (SvelteKit 3)
import adapter from '@sveltejs/adapter-node';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
sveltekit({
adapter: adapter(),
// Only origins that legitimately post forms to you, such as a payment page
csrf: { trustedOrigins: ['https://checkout.example-payments.com'] }
})
]
});Questions
Pourquoi ça marche avec npm run dev, mais pas en production?
La vérification CSRF de SvelteKit ne s’exécute que dans les versions de production. vite dev ne compare jamais les origines : une mauvaise ORIGIN ou un mandataire mal réglé n’apparaît qu’une fois déployé.
Faut-il régler ORIGIN avec adapter-node?
Pas derrière un mandataire qui transmet le Host d’origine et sert en HTTPS, ce qui est le cas sur Canner. Réglez-la quand le mandataire réécrit Host sans qu’on puisse le corriger, ou quand vous servez en http simple.
Où est passée la variable ORIGIN dans SvelteKit 3?
adapter-node 6 l’a retirée. Réglez paths.origin dans les options de sveltekit() de vite.config.js si vous avez besoin d’une origine fixe; elle est figée à la compilation.
Peut-on régler csrf.checkOrigin à false sans risque?
Non, et SvelteKit 3 a retiré l’option. Couper la vérification permet à n’importe quel site d’envoyer des formulaires au nom de vos utilisateurs connectés. Faites plutôt confiance à des origines précises avec csrf.trustedOrigins.
Déployez SvelteKit sans réglage d’origine à rater.
Canner exécute adapter-node derrière un mandataire qui conserve votre hôte et sert en HTTPS. Déployez depuis Git ou avec l’interface en ligne de commande.