Cookies are one of those browser fundamentals that feel deceptively simple until you ship them to production and discover security holes. In SvelteKit, the cookie API is straightforward, but the devil lives in the configuration—one missing flag can leave your authentication tokens exposed to XSS attacks, or your state vulnerable to CSRF.
We'll walk through cookies in SvelteKit: a practical Svelte & SvelteKit guide that covers setting and reading them safely, understanding httpOnly and secure flags, why path must be set explicitly, and how sameSite prevents CSRF attacks.
Setting cookies with cookies.set in actions and loads
In SvelteKit, cookies live behind the cookies object, available in server-side code only. You'll encounter it in two main contexts: inside server-side load functions and within form actions.
Inside a load function, setting a cookie typically happens after some logic completes—perhaps after fetching user preferences or validating a session. Here's a practical example where we set a theme preference:
// src/routes/+page.server.ts
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ cookies }) => {
const theme = cookies.get('theme') || 'light';
return {
theme
};
};
More commonly, you'll set cookies inside form actions. This is where most authentication and state changes happen. Let's say we have a login flow:
// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions: Actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
// Validate credentials (your auth logic here)
const user = await authenticateUser(email, password);
if (!user) {
return fail(401, { message: 'Invalid credentials' });
}
// Set the session cookie
cookies.set('sessionId', user.sessionToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24 * 7 // 7 days
});
redirect(303, '/dashboard');
}
};
Notice that cookies.set returns nothing—it's a side effect. The SvelteKit runtime handles sending the Set-Cookie header to the browser. If you call cookies.set multiple times in the same request, all cookies are batched into the response.
Reading cookies is just as straightforward: const token = cookies.get('sessionId'). But the real security comes from how you configure that cookie.
httpOnly and secure flags: protecting against XSS
The httpOnly flag is your first line of defense against cross-site scripting (XSS) attacks. When set to true, JavaScript running in the browser cannot access the cookie. This means if an attacker injects malicious script, they cannot steal your authentication tokens.
Here's what happens without httpOnly:
// Vulnerable: no httpOnly
cookies.set('sessionId', token, {
path: '/'
});
// Attacker's injected script can do this:
// const stolen = document.cookie; // Gets sessionId!
With httpOnly: true, that attack fails silently—the browser simply doesn't expose the cookie to JavaScript.
The secure flag ensures the cookie is only sent over HTTPS. In development with localhost, SvelteKit relaxes this requirement, but in production, always set secure: true:
cookies.set('sessionId', token, {
httpOnly: true,
secure: true, // Only sent over HTTPS
path: '/',
sameSite: 'lax'
});
Without the secure flag, a man-in-the-middle attacker on unencrypted WiFi can intercept the cookie during transmission. This is especially critical for authentication tokens.
A common pattern in SvelteKit applications is to use environment-based configuration for development:
const isProduction = process.env.NODE_ENV === 'production';
cookies.set('sessionId', token, {
httpOnly: true,
secure: isProduction, // true in prod, false locally
path: '/',
sameSite: 'lax'
});
Why path must be set explicitly
The path attribute determines which routes can access a cookie. If you don't set it, the browser defaults to the path where the cookie was set. This can lead to surprising behavior.
Imagine you set a cookie from the /admin/login route without specifying path:
// Bad: path not set, defaults to /admin/login
cookies.set('sessionId', token, {
httpOnly: true,
secure: true,
sameSite: 'lax'
});
// The cookie only gets sent to:
// /admin/login
// /admin/login/nested
// But NOT to /dashboard or /
Your app breaks because the session cookie isn't available on other routes. The fix is explicit:
// Good: path set explicitly
cookies.set('sessionId', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/' // Available to entire app
});
In nearly all cases, you want path: '/' for application-wide cookies like sessions. There are rare exceptions—say you have a multi-tenant app and want to isolate cookies per tenant path—but those require deliberate design.
A practical rule: always set path explicitly. Don't rely on defaults. The few seconds it takes prevents debugging sessions later.
sameSite to prevent CSRF attacks
The sameSite attribute protects against Cross-Site Request Forgery (CSRF). It controls whether the browser sends the cookie when a request originates from a different site.
There are three values: strict, lax, and none.
sameSite: 'strict' never sends the cookie in cross-site requests. This is safest but can break legitimate flows. For example, if a user clicks a link from an email to your site, the cookie isn't sent on that initial request:
cookies.set('sessionId', token, {
httpOnly: true,
secure: true,
path: '/',
sameSite: 'strict' // Most restrictive
});
sameSite: 'lax' is the sweet spot. It sends the cookie on top-level navigation (like following a link) but not on embedded requests (like form submissions from another site). This prevents CSRF while allowing normal browser flow:
cookies.set('sessionId', token, {
httpOnly: true,
secure: true,
path: '/',
sameSite: 'lax' // Good default
});
sameSite: 'none' sends the cookie everywhere, but requires secure: true. Use this only when you intentionally want cross-site access (like embedded widgets). It's rare:
cookies.set('sessionId', token, {
httpOnly: true,
secure: true, // Required when sameSite is 'none'
path: '/',
sameSite: 'none'
});
For authentication cookies, sameSite: 'lax' is the standard. It balances security with usability.
Common mistakes
- Forgetting httpOnly on auth cookies: This is the most common mistake. Without it, any XSS vulnerability exposes your tokens. Always set
httpOnly: truefor session and auth cookies. - Setting secure: false in production: Environment-check this carefully. A cookie with
secure: falsein production is a security vulnerability waiting to happen. - Omitting path: Relying on default behavior creates hard-to-debug issues. Explicit is better than implicit.
- Using sameSite: 'none' by default: Only use
nonewhen you have a specific cross-site requirement.laxis the right default. - Setting cookies on the client: Cookies set from browser code (via JavaScript) are automatically
httpOnly: falseandsecure: falsein many contexts. Use server-side actions and loads instead. - Not handling cookie expiration: Set
maxAgeorexpiresto avoid stale sessions. For sensitive tokens, shorter lifespans are better.
Key takeaways
- Use
cookies.setandcookies.getonly in server-side code: load functions and form actions. - Always set
httpOnly: trueandsecure: truefor authentication and session cookies to prevent XSS and man-in-the-middle attacks. - Explicitly set
path: '/'for application-wide cookies; don't rely on browser defaults. - Use
sameSite: 'lax'as the default for auth cookies to prevent CSRF while preserving normal navigation. - Handle cookie expiration with
maxAgeorexpiresto manage session lifetimes. - Test your cookie configuration in both development and production environments; behavior differs, especially around the
secureflag.






Comments
Loading comments…