kit
kit copied to clipboard
csrf.check_origin fails in dev mode when Vite runs behind SSL proxy server
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
Having this issue as well, it really makes debugging both localhost/domain a pain :/
I believe this is similar to https://github.com/sveltejs/kit/issues/6676
Hopefully this is addressed soon, it's practically impossible to test this on dev because of this.
@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)
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()
};
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
}
}
Bump
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.
Does any related PR exist to have similar behavior on providing proto based on the configuration in dev mode?
A solution I found was just to set the Origin header in the
caddyreverse 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.
this is linked to https://github.com/orgs/vercel/discussions/3390
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).
A solution I found was just to set the Origin header in the
caddyreverse 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