kit: Cross-site POST form submissions are forbidden

Describe the bug

I’m getting an error message about a cross-site request when submitting a form to a relative URL that’s handled by an endpoint on the same server.

The error only occurs when running the production build with the node-adapter, not with the dev server.

Reproduction

https://github.com/asoltys/svelte-cross-site-repro

Logs

Cross-site POST form submissions are forbidden

System Info

System:
    OS: Linux 5.19 Pop!_OS 22.04 LTS
    CPU: (12) x64 Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
    Memory: 4.55 GB / 15.35 GB
    Container: Yes
    Shell: 5.1.16 - /bin/bash
  Binaries:
    Node: 18.7.0 - ~/.local/share/pnpm/node
    npm: 8.15.0 - ~/.local/share/pnpm/npm
  Browsers:
    Chrome: 104.0.5112.101
    Firefox: 103.0
  npmPackages:
    @sveltejs/adapter-auto: next => 1.0.0-next.71 
    @sveltejs/adapter-node: 1.0.0-next.88 => 1.0.0-next.88 
    @sveltejs/kit: next => 1.0.0-next.470 
    svelte: ^3.44.0 => 3.50.0 
    vite: ^3.1.0 => 3.1.0

Severity

serious, but I can work around it

Additional Information

No response

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 10
  • Comments: 32 (2 by maintainers)

Most upvoted comments

Cross-site POST form submissions are forbidden

Use @sveltejs/adapter-node adapter, and pass ORIGIN=https://yoursite.com environment variable while deploying/building it. Here’s a proper solution for it.

@geoffrich thanks for reply. the issue i was having was just local. once deployed i dont have this issue. adding this to svelte.config.js while developing solves the issue:

csrf: { checkOrigin: false, }

I’m using adapter-vercel since my app’s running on vercel and I have this issue… any solutions for the ones that doesn’t use adapter-node?

Can someone please know how to handle this if the app is served from multiple origins (subdomains like *.mysite.com) ?

I worked around the multiple ORIGIN issue by modifying SvelteKit’s internal CSRF implementation to allow multiple origins in hooks.server.ts:

import { error, type Handle, type RequestEvent } from "@sveltejs/kit";
import { allowedOrigins } from "$lib/config";

const csrf = (
    event: RequestEvent,
    allowedOrigins: string[],
) => {
    const { request, url } = event;

    const forbidden =
        isFormContentType(request) &&
        (request.method === "POST" ||
            request.method === "PUT" ||
            request.method === "PATCH" ||
            request.method === "DELETE") &&
        !allowedOrigins.includes(request.headers.get("origin") || "");

    if (forbidden) {
        error(403, `Cross-site ${request.method} form submissions are forbidden`);
    }
};

function isContentType(request: Request, ...types: string[]) {
    const type = request.headers.get("content-type")?.split(";", 1)[0].trim() ?? "";
    return types.includes(type.toLowerCase());
}
function isFormContentType(request: Request) {
    // These content types must be protected against CSRF
    // https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/enctype
    return isContentType(
        request,
        "application/x-www-form-urlencoded",
        "multipart/form-data",
        "text/plain",
    );
}


export const handle: Handle = async ({ event, resolve }) => {
    csrf(event, allowedOrigins);
    return await resolve(event);
};

You’ll also have to set csrf: false in svelte.config.js, as we’ve re-implemented it here.

Inspiration from this thread.

It might not apply to many people, but I thought I’d share my case. The issue occurred when I made a fetch request from the browser to an endpoint defined in +server.js (which was actually a ts file). If the 'Content-Type': 'application/json' header wasn’t included as a following, it seemed to be treated as a default action, which likely caused this message to appear. ORIGIN in .env file not relevant in this case. You can success to fetch even when the server in behind the reverse proxy such as Ngrok or Cloudflare tunnel and the ORIGIN is http://localhost:5173.

// no
await fetch(location.origin, {
    method: 'post',
    body: JSON.stringify({data: res.data} satisfies MyData),
});

// ok
await fetch(location.origin, {
    method: 'post',
    body: JSON.stringify({data: res.data} satisfies MyData),
    headers: {
        'Content-Type': 'application/json'
    }
});

I hope this information will be useful to future readers.

I’m just previewing on localhost but I added a .env file like in the docs and that helped me:

ORIGIN=http://localhost:3000

And while on development how can I set the ORIGIN env variable?

I used VITE_ORIGIN and ORIGIN in my .env file and both seem to have no effect.

Looks like that’s still an open issue: https://github.com/sveltejs/kit/issues/8026

How are you running the server? I would bet the issue is that you’re not using https://github.com/sveltejs/kit/tree/master/packages/adapter-node#environment-variables to tell the server what origin it’s serving from. Note that in particular in production the Node adapter will assume by default that it has an HTTPS proxy in front of it and that its origin is https://[host header], not http://[host header].

If the 'Content-Type': 'application/json' header wasn’t included as a following, it seemed to be treated as a default action, which likely caused this message to appear. ORIGIN in .env file not relevant in this case.

Thank you @sundaycrafts, this was useful for me.

Hope I help someone,

Im running my app with node adapter through docker

in development I put in docker file

# Assuming the default app runnning host:port
ENV ORIGIN=http://localhost:3000 

or in docker-compose file

version: '3.8'
services:
  mysveltenodeapp-deploy:
    image: mysveltenodeapp
    ports:
      - '3000:3000'
    environment:
      - ...
      - ORIGIN=http://localhost:3000

In my case the issue was present at local dev environment but not on production. My dev environment runs behind Nginx reverse proxy.

The way I fixed it is by replacing https:// with http:// in the Origin header:

 proxy_set_header Origin http://$http_host;

Something similar might work with other reverse proxy configurations.

It seems Vite completely ignores the x-forwarded-proto and SSL-Offloaded headers so these don’t have any effect.

To debug the issue you can compare the request origin header and the url.origin property here: node_modules/@sveltejs/kit/src/runtime/server/respond.js:63

Hope this helps someone!

How to handle multiple ORIGINs?

I’m just previewing on localhost but I added a .env file like in the docs and that helped me:

ORIGIN=http://localhost:3000

Thank you for being specific and not sending me on a wild goose chase.

And while on development how can I set the ORIGIN env variable?

I used VITE_ORIGIN and ORIGIN in my .env file and both seem to have no effect.