Fait partie de Hébergement par cadricielHébergement · SvelteKit

« 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 forbidden

Pourquoi ça commence après un déploiement

Ce qui s’est passéPourquoi les origines diffèrentCorrectif
ORIGIN=http://localhost:3000 a suivi depuis un fichier .env †Le navigateur envoie https://votre-domaineRetirez 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 origineLaissez-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 httpsRé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 amontAjoutez 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’effetRé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 siteAjoutez-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 avec SvelteKit 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, -netlify et -cloudflare sont 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 Host tel quel et définit X-Forwarded-Proto, X-Forwarded-For et X-Forwarded-Host. Par défaut, Caddy écarte les valeurs X-Forwarded-* envoyées par un client et écrit les siennes.
  • Les valeurs par défaut fonctionnent donc. Sans ORIGIN ni paths.origin, adapter-node construit https:// 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 aussi Host.
  • Ne réglez pas ORIGIN à partir de `CANNER_URL`. CANNER_URL est l’adresse canner.app du déploiement (ou celle de l’aperçu) : les visiteurs de votre domaine personnalisé recevraient l’erreur 403. Même chose pour paths.origin.
  • Vous voulez être explicite? PROTOCOL_HEADER=x-forwarded-proto et HOST_HEADER=x-forwarded-host lisent 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.