Skip to content

Instantly share code, notes, and snippets.

@Maxiviper117
Last active October 31, 2025 10:35
Show Gist options
  • Select an option

  • Save Maxiviper117/95a31750b74510bbb413d2e4ae20b4e8 to your computer and use it in GitHub Desktop.

Select an option

Save Maxiviper117/95a31750b74510bbb413d2e4ae20b4e8 to your computer and use it in GitHub Desktop.
Guide: Implementing CSRF Protection in SvelteKit

Guide: CSRF Protection in SvelteKit

Cross Site Request Forgery (CSRF) is when a browser sends a state changing request to your app without the user intending to do so. SvelteKit already ships with CSRF checks based on the request origin. In most cases you should keep that. Use a custom hook only when you need finer control.


1. Configure SvelteKit CSRF

📂 svelte.config.ts

import adapter from '@sveltejs/adapter-auto';

const config = {
  kit: {
    adapter: adapter(),
    csrf: {
      trustedOrigins: [
        'https://trusted-site.com',
        'http://localhost:5173'
      ]
    }
  }
};

export default config;

What this does

  • Same origin form posts are allowed.
  • Form posts from the trusted origins above are also allowed.
  • Built in protection stays active.
  • Disabling checkOrigin should be a conscious choice, not the default.

  1. Optional custom CSRF hook

Use this when you want to require both a trusted origin and a specific path.

📂 src/hooks/csrf.ts

import type { Handle } from '@sveltejs/kit';
import { json, text } from '@sveltejs/kit';

export function csrf(
  allowedPaths: string[],
  allowedOrigins: string[] = []
): Handle {
  return async ({ event, resolve }) => {
    const { request, url } = event;
    const requestOrigin = request.headers.get('origin');
    const isSameOrigin = requestOrigin === url.origin;

    const isAllowedOrigin =
      requestOrigin != null && allowedOrigins.includes(requestOrigin);

    const isAllowedPath = allowedPaths.some((p) => {
      return url.pathname === p || url.pathname.startsWith(p + '/');
    });

    const isForm =
      isFormContentType(request) &&
      ['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method);

    const forbidden =
      isForm &&
      !isSameOrigin &&
      !(isAllowedOrigin && isAllowedPath);

    if (forbidden) {
      const message = `Cross site ${request.method} form submissions are forbidden`;
      if (request.headers.get('accept') === 'application/json') {
        return json({ message }, { status: 403 });
      }
      return text(message, { status: 403 });
    }

    return resolve(event);
  };
}

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) {
  return isContentType(
    request,
    'application/x-www-form-urlencoded',
    'multipart/form-data',
    'text/plain'
  );
}

Notes

  • Path check supports nested paths.
  • Origin and path must both match for cross site form posts.
  • Only form style requests are checked.

  1. Integrate in hooks.server.ts

📂 src/hooks.server.ts

import { sequence } from '@sveltejs/kit/hooks';
import { csrf } from './hooks/csrf';

const allowedPaths = ['/api/public-form'];
const allowedOrigins = ['https://trusted-site.com', 'http://localhost:5173'];

export const handle = sequence(
  csrf(allowedPaths, allowedOrigins)
  // other handlers can go here
);

If you keep checkOrigin: true in svelte.config.ts this hook will be an additional layer. If you set checkOrigin: false this hook will be the only layer and must be maintained carefully.

  1. Test matrix

Allowed

Scenario Why Same origin POST to /api/contact Same origin allowed https://trusted-site.com POST to /api/public-form Origin trusted and path allowed http://localhost:5173 POST to /api/public-form/step-2 Origin trusted and path prefix matches

Blocked

Scenario Why https://malicious.com POST to /api/contact Origin not trusted https://trusted-site.com POST to /api/private-form Path not allowed Cross site PUT from unknown origin Origin not trusted and path not allowed

  1. Important limitations
  • This protects only form like requests that use typical browser form content types. If you accept JSON requests that change data you should add a token based CSRF approach or require a custom header.
  • If your app sits behind a proxy or platform that rewrites Origin or Host you must fix that first, otherwise these checks will reject valid requests.
  • Keep the list of trusted origins and allowed paths small.

  1. When to use a token

Use token based CSRF when:

  • browsers call your JSON endpoints with credentials
  • you expose endpoints to multiple front ends
  • you cannot trust Origin headers in your deployment

A token approach is more work but protects more kinds of requests.

Final shape

  1. Keep SvelteKit CSRF on.
  2. Add trustedOrigins for specific external sites and for local development.
  3. Add the custom hook if you need origin plus path rules.
  4. Log 403s and review them.
@Coinhexa
Copy link

What does this setup look like if your backend is a separate express server running on 3002 while your sveltekit frontend runs omn 5173

@Maxiviper117
Copy link
Author

@Coinhexa In that case, since all your requests to the separate backend are cross-origin, the safest approach would be to proxy all requests through SvelteKit using its internal API to handle the fetching. This way, all requests from the front end would appear as same-origin.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment