Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Cross-site POST form submissions are forbidden" #54

Open
LeqitDev opened this issue Apr 14, 2024 · 14 comments
Open

"Cross-site POST form submissions are forbidden" #54

LeqitDev opened this issue Apr 14, 2024 · 14 comments

Comments

@LeqitDev
Copy link

Just encountered this error while testing my website in production.

Setup:
I'm using docker with nginx proxy manager as a reverse proxy.

So i was building my multi-container through this docker-compose.yml

version: '3.9'
services:
  db:
    restart: 'always'
    build: './pocketbase'
    expose:
      - 80
    ports:
      - ${PORTDB:-8081}:80
    environment:
      FRONTEND_URL: http://web:3001
    volumes:
      - dbdata:/pb/pb_data
      - ./pocketbase/pb_migrations:/pb/pb_migrations
      - ./pocketbase/pb_hooks:/pb/pb_hooks
    networks:
      - nginx-network
  web:
    restart: 'always'
    build: 
      context: './web'
      dockerfile: 'Dockerfile'
      args:
        PUBLIC_DATABASE: ${PUBLIC_DATABASE:-https://api.domain.tld/sub}
    ports:
      - ${PORTWEB:-8080}:3001
    expose:
      - 3001
    environment:
      PORT: 3001
      PUBLIC_DATABASE: ${PUBLIC_DATABASE:-https://api.domain.tld/sub}
      ORIGIN: https://sub.domain.tld
    networks:
      - nginx-network

volumes:
  dbdata:

networks:
  nginx-network:
    name: nginx-proxy-manager_default
    external: true

With my web Dockerfile looking like this:

FROM oven/bun as builder

ARG PUBLIC_DATABASE
ENV PUBLIC_DATABASE=${PUBLIC_DATABASE}

WORKDIR /app

COPY . .
ENV ORIGIN=https://sub.domain.tld

RUN bun i
RUN bun run build

FROM oven/bun
COPY --from=builder /app/build .

ENV ORIGIN=https://sub.domain.tld

EXPOSE 3001

CMD [ "bun", "index.js" ]

I know that the ORIGIN env is set multiple times just wanted to check that it wasn't on the wrong spot.

I was also printing the url.origin in the load function in +layout.server.ts which resulted in the following output:
http://sub.domain.tld

Running bun --print process.env also tried with (Bun.env) resulted in:

{
  ...,
  ORIGIN: "https://sub.domain.tld",
  ...,
}

What i discovered was that when i didnt enforce https with my reverse proxy and use the website with "http://" it just works fine but that isnt the way i think

Hope somebody can help fixing this monster that cost me more then 2 hours of trying and failing
If you need some more infos just ask for it

@YummYume
Copy link

YummYume commented May 2, 2024

Hello,

I'm having the exact same issue. My configuration is very similar to yours and is also using Docker. I tried a lot of things, but only switching to the Node adapter worked. This is what I pretty much always end up doing because of a few incompatibilities that this adapter seems to have.

Here my Dockerfile if needed, I kept my original configuration but added Node with COPY (for Alpine) and changed the adapter in my svelte.config.js file.

@maietta
Copy link

maietta commented Jul 16, 2024

This is how I'm getting around the issue at the moment. I don't like doing this, but it's about the only wait that doesn't involve throwing yet another server in front of the app that fixes this.

My svlete.config.js

import adapter from 'svelte-adapter-bun';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	preprocess: vitePreprocess(),

	kit: {
		adapter: adapter(),
		csrf: { // Did https://github.com/gornostay25/svelte-adapter-bun/pull/61 work? (I can't wait to find out)
			checkOrigin: false
		}
	}
};

export default config;

The server option would be to use something like this in front of your app with something like this:

// @bun
import { build_options, env, handler_default } from './build/handler.js';
import './build/mime.conf.js';

var { serve } = globalThis.Bun;
/*! MIT © Volodymyr Palamar https://github.com/gornostay25/svelte-adapter-bun */
var hostname = env('HOST', '0.0.0.0');
var port = parseInt(env('PORT', 3000));
var { httpserver, websocket } = handler_default(build_options.assets ?? true);
var serverOptions = {
    baseURI: env('ORIGIN', undefined),
    fetch: httpserver,
    hostname,
    port,
    development: env('SERVERDEV', build_options.development ?? false),
    websocket
};
websocket && (serverOptions.websocket = websocket);

console.info(`Listening on ${hostname + ':' + port}` + (websocket ? ' (Websocket)' : ''));

serve(serverOptions);

Instead of running your app directly, you'd bun server.ts instead. This in turn will run your app fixing the CORS issue.

@vyconm
Copy link

vyconm commented Aug 26, 2024

bump this issue still persists, might be related or fixable py this pr: #61

@jack-y
Copy link

jack-y commented Sep 26, 2024

Please fix this issue, it's still blocking our production release.

@notramo
Copy link

notramo commented Oct 2, 2024

Just reminding everyone here that origin checking is an insufficient CSRF protection measure. It's harmful by creating a false sense of security. Origin can be spoofed, so checking it only provides partial protection, but doesn't protect against sophisticated attacks.

Feel free to set checkOrigin: false and add a CSRF token field to every form instead for proper prevention.

Edit: since #61 is merged, disabling origin checking may be not necessary anymore, but it's still an insufficient security measure, and CSRF token is still required.

Edit again: I was wrong about the necessity of CSRF tokens, see my below comment #54 (comment)

@jack-y
Copy link

jack-y commented Oct 2, 2024

@notramo Thank you so much for this comment.
For all, if needed, here is a good article by Admir Dizdar on the CSRF token and its management:
What is a CSRF Token and How Does It Work?

@andersmmg
Copy link

I'm still getting this error with the latest version of the adapter in my SvelteKit action. It works with the node adapter

@YpsilonTM
Copy link

YpsilonTM commented Nov 17, 2024

I have the same issue, currently not smart enough to fix this with a pr 😓 Has #61 fixed this issue? I see its merged, But if so can a new release be build?

@fubits1
Copy link

fubits1 commented Dec 11, 2024

Can confirm that issue still persists and @maietta's workaround (csrf: {checkOrigin: false}) helps for the time being

I'm using:

  • Bun 1.1.38
  • svelte-adapter-bun@0.5.2
  • @sveltejs/kit@2.10.1
  • svelte@5.10.1

@maietta
Copy link

maietta commented Dec 11, 2024

Can confirm that issue still persists and @maietta's workaround (csrf: {checkOrigin: false}) helps for the time being

I'm using:

  • Bun 1.1.38
  • svelte-adapter-bun@0.5.2
  • @sveltejs/kit@2.10.1
  • svelte@5.10.1

Just to be clear, this, turning off CRSF origin checking is a bad idea and can lead to bigger issues. I would recommend the server wrapper instead until the underlying issue has been resolved.

@mgaroz
Copy link

mgaroz commented Dec 16, 2024

Setting both PROTOCOL_HEADER=x-forwarded-proto and HOST_HEADER=x-forwarded-host env vars is what fixed it for me

@Ileies
Copy link

Ileies commented Dec 19, 2024

@YpsilonTM You can use it this way in your package.json:
"svelte-adapter-bun": "git+https://github.com/gornostay25/svelte-adapter-bun.git#ade8792",

The problem is that this doesn't fix it. I traced the error back to build/handler.js:677 where the request already comes as HTTP and right there could be changed to HTTPS as in merge #61. The thing is, while the merge included the neccessary changes for handler.js, when building the project, there's still the old version in the build folder. It contains the improved version somewhere nested but doesn't use it.

@notramo
Copy link

notramo commented Dec 19, 2024

Just reminding everyone here that origin checking is an insufficient CSRF protection measure. It's harmful by creating a false sense of security. Origin can be spoofed, so checking it only provides partial protection, but doesn't protect against sophisticated attacks.

Feel free to set checkOrigin: false and add a CSRF token field to every form instead for proper prevention.

Turns out I was wrong. Origin can not be spoofed if the scheme (https://) and port is also checked, i.e. the entire origin, not only the domain name. I wrote my above comment based on an old article which detailed a CSRF vulnerability in popular sites which resulted from the sites only checking the domain but not the scheme, and it was possible to spoof with e.g. the data: scheme and the domain would still match. However, if the scheme is checked, it's OK. The article was also an old one, and browsers gained other protection measures since.
CSRF token is outdated, and it has to balance UX and security (session-wide CSRF tokens provide less security, per-request CSRF tokens has worse UX). Newer protection measures provide the same (or in certain cases stronger) security level of per-request CSRF token, while having the easy UX of session-wide CSRF tokens.

Origin checking is a modern and good protection (again, only if it checks the entire Origin with scheme and port, not only the domain).
When it's not available, e.g. this issue, here are some modern alternatives. All browsers support these since quite a few years (the older versions which don't support them contain severe RCE exploits, so users should not use those anyways).

Cookie attributes and name prefixes

SameSite=Strict prevents the cookie being sent with cross-site requests.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#controlling_third-party_cookies_with_samesite

I also recommend setting the __Host- prefix which mandates some other attributes like Secure and HttpOnly. The browser discards cookies that has name starting with __Host- but not setting the required attributes.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#cookie_prefixes

__Host- is the highest available cookie security level, but there is no reason to use lower security.

Note that Google Chrome does not treat localhost as secure origin and drops the cookie (because of the Secure flag), so in Vite localhost dev mode you should either omit the prefix and Secure, or use Firefox / Safari. I have read something about a CLI option for Chrome to treat localhost as secure, but I couldn't try it as Chromium is not supported on musl libc systems.

Sec-Fetch-Site metadata

This is a newer feature (only supported since 2023 in major browsers), so I recommend adding it as a defense-in-depth measure, but always configure the __Host- name prefix for cookies.

Browsers send metadata about the request which can be used to filter out unacceptable requests.
In most cases, a POST request must have the Sec-Fetch-Site with a same-origin. Note that Sec-Fetch-Mode also has a same-origin value, but that doesn't offer protection by itself, as same-origin navigation (hard reload POST, as opposed to use:enhance which uses fetch()) has the value navigate so you should enable navigate too but the navigate value is also used for cross-site navigation, which would enable CSRF. So Sec-Fetch-Site is the header used for CSRF protection.

Here's a more detailed tutorial, which covers other use cases for these headers (not only CSRF protection).
https://web.dev/articles/fetch-metadata

Content Security Policy - form-action

If you want to make the site super secure, you can consider same-origin form submission attacks. E.g. an unprivileged user hijacks a page, and an admin user submits a form to a wrong endpoint. E.g. a comment form is submitted to an admin role management endpoint, granting elevated the attacker (both the comment form and admin form are on the same origin).

CSP form-action can be used for this, however it's difficult to use in this way with the current SvelteKit CSP mechanism, which is meant for site-wide configuration and not per-page. Hopefully, in future SvelteKit versions, there will be more fine-grained control for CSP.

Further reading: https://lcamtuf.coredump.cx/postxss/

Security scanners

These are not fully related to CSRF but since we are talking about security, I think it's useful to mention some tools.

I recommend scanning the site with the MDN Observatory tool. It helps to set up more secure defaults, which protects from other attacks too (e.g. XSS and protocol downgrade).
Aim for a 100+ score. It's easily possible with a correctly configured site. Maximum achievable is probably 140 points.
Scan multiple pages, as the results are per-page, not site-wide.
https://developer.mozilla.org/en-US/observatory

Also, don't forget to always set up HSTS preload.
Check the headers (and submit your site) here: https://hstspreload.org
MDN - Strict-Transport-Security
Quick summary if you don't want to read the MDN article: set the value of the strict-transport-security header to max-age=252460800; includeSubDomains; preload on every domain and subdomain, then don't forget to submit the site for preload. I recommend setting the header in the Caddy or River config instead of SvelteKit, so it's applied to every request.

@fubits1
Copy link

fubits1 commented Jan 29, 2025

My setup is a bit more complex but I observed something so maybe it can help solve this issue.

I'm deploying behind Traefik to a subdomain + path (e.g. https://subdomain.example.com/project). On the /project route there is a login SvelteKit action. This is where the CSRF issue arises.

I'm also using paraglide for i18n which runs in a hook or rather sequence of hooks sequence(originalHandle, authGuard, handleParaglide). Maybe the sequence causes the issue in my case. But maybe not.

Couldn't fix it with adapter-bun - neither ORIGIN, nor PROTOCOL_HEADER + HOST_HEADER (and optionally PORT_HEADER=x-forwarded-port) worked. I see in the client request that origin (lowercase) is set correctly, but none of the other headers exist.

Switched back to adapter-node and it works out of the box now (I'm not even setting ORIGIN).

Now I see following x-headers when logging the reguest in the action (=server-side) using adapter-node:

    'x-forwarded-for': '94.xxx.xxx.xxx',
    'x-forwarded-host': 'subdomain.example.com',
    'x-forwarded-port': '443',
    'x-forwarded-proto': 'https',
    'x-forwarded-server': 'a-12-characters-hash',
    'x-real-ip': '94.xxx.xxx.xxx',
    'x-sveltekit-action': 'true'

What is it with x-forwarded-for and x-forwarded-server? Maybe these should be set as ENV vars as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests