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)