kit icon indicating copy to clipboard operation
kit copied to clipboard

csrf.check_origin fails in dev mode when Vite runs behind SSL proxy server

Open georgecrawford opened this issue 2 years ago • 18 comments

Describe the bug

In dev mode, SvelteKit adds a Vite middleware which determines a base URL for the event.url that's passed down through request handlers:

https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/vite/dev/index.js#L268-L270

const base = `${vite.config.server.https ? 'https' : 'http'}://${req.headers[':authority'] || req.headers.host}`;

My local dev setup uses a Nginx proxy, which provides SSL and routes traffic to the SvelteKit Vite server on an internal port. I can log the difference between the request that Vite and SvelteKit handle, and the x-forwarded... headers in, for example, a SvelteKit hook:

A server hook:

export default async ({event, resolve}) => {
    console.log({
        'event': `${event.url.protocol}//${event.url.host}`,
        'trusted origin from headers': `${request.headers.get('x-forwarded-proto')}://${request.headers.get('x-forwarded-host')}`,
    });
    ...
}

Prints:

{
  'event': 'http://client.dev.domain.com',
  'trusted origin from headers': 'https://client.dev.domain.com'
}

This causes problems in the check for CSRF:

https://github.com/sveltejs/kit/blob/master/packages/kit/src/runtime/server/index.js#L43-L56

if (options.csrf.check_origin) {
	const forbidden =
		request.method === 'POST' &&
		request.headers.get('origin') !== url.origin &&
		is_form_content_type(request);

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

As far as I can tell, there's no way to instruct the Vite dev server (using SvelteKit's middleware) that it's running behind a proxy. For this CSRF check to work correctly in dev, we'd need the equivalent of the PROTOCOL_HEADER and HOST_HEADER environment variables that the adapter-node packages reads, in order to declare that the proxy server is 'trusted'.

Constructing a base for the URL constructor which relies on vite.config.server.https is misleading, as there are multiple ways in which the Origin header might have a https protocol - including the reserve proxy I've described.

This is essentially the inverse of https://github.com/sveltejs/kit/issues/6589, in that it only affects Vite dev mode. I could of course disable config.kit.csrf.checkOrigin in dev mode, but I'd prefer not to.

Reproduction

I can't easily link to a reproduction as I need a proxy server in front of SvelteKit. But I think the explanation above should be clear.

Logs

No response

System Info

System:
    OS: macOS 12.4
    CPU: (8) arm64 Apple M1 Pro
    Memory: 206.78 MB / 32.00 GB
    Shell: 3.2.57 - /bin/bash
  Binaries:
    Node: 16.18.0 - /usr/local/bin/node
    Yarn: 1.22.10 - /usr/local/bin/yarn
    npm: 8.19.2 - /usr/local/bin/npm
    Watchman: 2022.11.14.00 - /opt/homebrew/bin/watchman
  Browsers:
    Chrome: 107.0.5304.110
    Chrome Canary: 110.0.5443.0
    Firefox: 105.0.3
    Safari: 15.5
  npmPackages:
    @sveltejs/adapter-node: ^1.0.0-next.102 => 1.0.0-next.102
    @sveltejs/kit: ^1.0.0-next.577 => 1.0.0-next.577
    svelte: ^3.53.1 => 3.53.1

Severity

serious, but I can work around it

Additional Information

No response

georgecrawford avatar Dec 09 '22 17:12 georgecrawford

Having this issue as well, it really makes debugging both localhost/domain a pain :/

Bewinxed avatar Feb 21 '23 15:02 Bewinxed

I believe this is similar to https://github.com/sveltejs/kit/issues/6676

eneault avatar May 04 '23 12:05 eneault

Hopefully this is addressed soon, it's practically impossible to test this on dev because of this.

danpaldev avatar May 22 '23 15:05 danpaldev

@georgecrawford Thank you for the very detailed explanation. I too am having this problem.

Being able to run development mode fronted by your webserver/proxy/ssl/load balancer/etc it of course a pretty important use case. (Long gone are the days of doing all local dev on http://localhost:8080 directly to your raw application server and knowing you system would be deploy-able to a live environment fronted by a simple web server to handle ssl)

leejoramo-d51 avatar May 26 '23 18:05 leejoramo-d51

This is a serious pain point, things are working in production because we can set the correct ORIGIN to our domain but if I were to test locally only GET or DELETE requests work but never multipart form POST request.

Right now, my only option is to do a check if app is running on development:

// in svelte.config.js

const config = {
  kit: {
    adapter: adapterNode(),
    csrf: {
      checkOrigin: process.env.NODE_ENV === 'development' ? false : true
    }
  },
  preprocess: vitePreprocess()
};

lnfel avatar Jun 24 '23 07:06 lnfel

A solution I found was just to set the Origin header in the caddy reverse proxy, instead of configuring it with the ORIGIN environment variable. The request URL is rewritten by the proxy, removing the https://, but the same was not done to the Origin header, which was the source of issues for me.

Example snippet from the Caddyfile:

https://localhost:5173 {
	reverse_proxy web:5173 {
		header_up Origin http://localhost:5173
	}
}

quentin-fox avatar Aug 12 '23 05:08 quentin-fox

Bump

Padi142 avatar Dec 16 '23 23:12 Padi142

Dealing with this too running vercel dev locally. Setting ORIGIN and other env variables does not solve it. I'd prefer not to disable csrf even in development. vercel dev runs on localhost:3000 by default.

nathancahill avatar Jan 02 '24 09:01 nathancahill

Does any related PR exist to have similar behavior on providing proto based on the configuration in dev mode?

iVegas avatar Feb 28 '24 16:02 iVegas

A solution I found was just to set the Origin header in the caddy reverse proxy, instead of configuring it with the ORIGIN environment variable. The request URL is rewritten by the proxy, removing the https://, but the same was not done to the Origin header, which was the source of issues for me.

Example snippet from the Caddyfile:

https://localhost:5173 {
	reverse_proxy web:5173 {
		header_up Origin http://localhost:5173
	}
}

this seems the best solution so far, thanks.

MyWay avatar Apr 09 '24 09:04 MyWay

this is linked to https://github.com/orgs/vercel/discussions/3390

dominicfarr avatar Apr 24 '24 13:04 dominicfarr

I'm not overly sure if there is any correlation here, however I was experiencing this issue in both Dev (behind NGROK) and in production (Cloudflare)

I found that the "checkOrigin" worked in Dev, but not in production. I later was able to resolve this in production by disabling Cloudflare's SSL Proxy, and just have the DNS record point directly to my production environment (Vercel).

AshleyJackson avatar Jun 06 '24 04:06 AshleyJackson

A solution I found was just to set the Origin header in the caddy reverse proxy, instead of configuring it with the ORIGIN environment variable. The request URL is rewritten by the proxy, removing the https://, but the same was not done to the Origin header, which was the source of issues for me.

Example snippet from the Caddyfile:

https://localhost:5173 {
	reverse_proxy web:5173 {
		header_up Origin http://localhost:5173
	}
}

For anyone using Nginx, here's what I added to solve the issue without disabling CSRF.

As far as I understand, this shouldn't create any possible CSRF, but please let me know if it does.

server {
    server_name abc.xyz;
    location / {
        proxy_pass http://127.0.0.1:5173;
        # ...
        proxy_set_header Origin http://$host # Add this line
        # ...
    }
}

proxy_set_header http://$http_host

lts20050703 avatar Jun 26 '24 17:06 lts20050703