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.
📂 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.
⸻
- 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.
⸻
- 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.
⸻
- 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
⸻
- 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.
⸻
- 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
- Keep SvelteKit CSRF on.
- Add trustedOrigins for specific external sites and for local development.
- Add the custom hook if you need origin plus path rules.
- Log 403s and review them.
What does this setup look like if your backend is a separate express server running on 3002 while your sveltekit frontend runs omn 5173