Cross-Site Request Forgery (CSRF) is a vulnerability that lets attackers trick your users into unintentionally submitting requests. This guide demonstrates how to implement robust CSRF protection in SvelteKit using custom middleware hooks.
π Step 1: Disable Built-in SvelteKit CSRF Check
SvelteKit provides default CSRF checks. To enable custom handling, disable the built-in check first.
π svelte.config.ts
// svelte.config.ts import adapter from '@sveltejs/adapter-auto'; export default { kit: { adapter: adapter(), csrf: { checkOrigin: false, // Disable built-in origin checking to allow custom middleware }, }, };
π‘οΈ Step 2: Create Custom CSRF Middleware
Create the custom CSRF middleware hook file to provide enhanced security and configurability.
π src/hooks/csrf.ts
// src/hooks/csrf.ts import type { Handle } from '@sveltejs/kit'; import { json, text } from '@sveltejs/kit'; /** * Custom CSRF Protection Middleware * * @param allowedPaths - List of URL paths that bypass CSRF protection. * @param allowedOrigins - Trusted origins allowed to make cross-origin form submissions. */ export function csrf(allowedPaths: string[], allowedOrigins: string[] = []): Handle { return async ({ event, resolve }) => { const { request, url } = event; // Get the 'origin' header from the incoming request const requestOrigin = request.headers.get('origin'); // Determine if the request comes from the same origin const isSameOrigin = requestOrigin === url.origin; // Check if the request origin is explicitly allowed (trusted external origins) const isAllowedOrigin = allowedOrigins.includes(requestOrigin ?? ''); // Define conditions under which the request is forbidden (potential CSRF attack) const forbidden = isFormContentType(request) && // Checks if the request contains form data ['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method) && // State-changing methods !isSameOrigin && // Origin mismatch !isAllowedOrigin && // Not explicitly allowed !allowedPaths.includes(url.pathname); // Path not explicitly allowed // If forbidden, return a 403 Forbidden response immediately if (forbidden) { const message = `Cross-site ${request.method} form submissions are forbidden`; // Return JSON or plain text based on request headers if (request.headers.get('accept') === 'application/json') { return json({ message }, { status: 403 }); } return text(message, { status: 403 }); } // If the request passes CSRF checks, continue to the next middleware or endpoint return resolve(event); }; /** * Helper function to check if request 'origin' is allowed. */ function isAllowedOrigin(requestOrigin: string | null, allowedOrigins: string[]) { return allowedOrigins.includes(requestOrigin ?? ''); } /** * Helper function to determine if request content-type indicates a form submission */ function isFormContentType(request: Request) { const type = request.headers.get('content-type')?.split(';', 1)[0].trim().toLowerCase() ?? ''; return ['application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain'].includes(type); } }
π© Step 3: Integrate the Middleware into SvelteKit's Hooks
Integrate this middleware using SvelteKit's sequence
helper, allowing you to chain multiple middleware cleanly.
π src/hooks.server.ts
// src/hooks.server.ts import { sequence } from '@sveltejs/kit/hooks'; import { csrf } from './hooks/csrf'; // Define paths exempt from CSRF checks (e.g., public forms or APIs) const allowedPaths = ['/api/public-form']; // Define trusted origins allowed to make cross-origin form submissions const allowedOrigins = ['https://trusted-site.com', 'http://localhost:5173']; // Export the combined hooks using 'sequence' for better flexibility export const handle = sequence( csrf(allowedPaths, allowedOrigins) // CSRF hook added here // You can chain additional middleware hooks here if needed );
β Step 4: Testing Your CSRF Middleware
Allowed Requests (should pass):
- Form submission from same-origin (e.g.,
https://your-site.com
βhttps://your-site.com/api
). - Cross-origin submission from explicitly allowed origin (
https://trusted-site.com
) to an explicitly allowed path (/api/public-form
).
Blocked Requests (should fail with 403
):
- Cross-origin submission from a non-whitelisted domain.
- Submission from an allowed origin (
https://trusted-site.com
) to a non-allowed path. - Submissions from unlisted origins/domains.
βοΈ Step 5: Integrate Using SvelteKit sequence
Hook
Use SvelteKitβs built-in sequence
function to combine your CSRF middleware with other hooks seamlessly.
π src/hooks.server.ts
// src/hooks.server.ts import { sequence } from '@sveltejs/kit/hooks'; import { csrf } from './hooks/csrf'; // Initialize middleware with explicit allowed paths and origins const csrfProtection = csrf( ['/api/public-form'], // paths exempt from CSRF protection ['https://trusted-site.com', 'http://localhost:5173'] // trusted cross-origin sites ); // Export the combined hook using SvelteKitβs 'sequence' export const handle = sequence(csrf(['/api/public-form'], ['https://trusted-site.com']));
β οΈ Important Formatting Notes for Allowed Origins
When defining allowed origins, remember:
β
Correct Examples:
['https://trusted-site.com', 'http://localhost:5173']
β Incorrect Examples (these won't work!):
- β No protocol:
'trusted-site.com'
- β Wildcards not supported:
'*.example.com'
- β Paths not allowed:
https://example.com/path
π Summary & Final Thoughts
You've successfully implemented custom CSRF protection in your SvelteKit application:
- β Custom middleware with explicit allowed origins and paths.
- β Flexible configuration via hooks.
- β Clear error messaging for blocked requests.
This approach provides powerful protection against CSRF attacks without restricting legitimate cross-origin usage.
Stay secure, and happy coding! πβ¨
Top comments (0)