Cookies are the oldest client-side storage mechanism, predating localStorage by over a decade. They were designed with one specific job: let the server recognize a returning client. That is still their best use case today, and the security attributes exist to make that job safe. Used correctly, cookies give you authenticated sessions that JavaScript cannot steal. Used incorrectly, they become attack vectors.
Understanding the security attributes is not optional if you are building anything with authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token..
What a cookieWhat is cookie?A small piece of data the browser stores and automatically sends with every request to the matching server, often used for sessions. actually is
A cookie is a small string (max 4KB) that the browser stores and automatically attaches to every matching HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. request. The flow looks like this:
Client → Server: POST /login (with credentials)
Server → Client: 200 OK
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax
Client → Server: GET /dashboard
Cookie: session=abc123 ← automatically added by browser
Server → Client: 200 OK (recognizes session)The server sets the cookie in the response. The browser stores it. Every subsequent request to that domain includes the cookie header automatically, you write zero JavaScript for this to work.
The security attributes
HttpOnlyWhat is httponly?A cookie flag that prevents JavaScript from reading the cookie. It stops XSS attacks from stealing session tokens or authentication cookies.
Marks the cookieWhat is cookie?A small piece of data the browser stores and automatically sends with every request to the matching server, often used for sessions. as inaccessible to JavaScript. document.cookie will not show it, and fetch cannot read it either. Only the browser's internal HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. stack can see it.
Set-Cookie: session=abc123; HttpOnlyThis single attribute eliminates the most common attack vector: XSSWhat is xss?Cross-Site Scripting - an attack where malicious JavaScript is injected into a web page and runs in other users' browsers, stealing data or hijacking sessions. (cross-site scripting). Even if an attacker injects malicious JavaScript into your page, they cannot steal an HttpOnly cookie.
localStorage.getItem('token') hands them your user's credentials. An HttpOnly cookie is completely invisible to that script.Secure
The cookie is only sent over HTTPSWhat is https?HTTP with encryption added, so data traveling between your browser and a server can't be read or tampered with by anyone in between. connections. On HTTP, the browser silently drops it.
Set-Cookie: session=abc123; SecureAlways combine Secure with HttpOnly for sensitive cookies. In local development over http://localhost, browsers typically exempt localhost from the Secure requirement.
SameSite
Controls whether the cookie is sent with cross-site requests. This is your primary CSRFWhat is csrf?Cross-Site Request Forgery - an attack where a malicious website tricks your browser into sending a request to another site where you're logged in. (cross-site request forgery) defense.
Set-Cookie: session=abc123; SameSite=Lax| Value | Behavior |
|---|---|
Strict | Cookie never sent on cross-site requests. Maximum protection, but breaks OAuth flows and email links. |
Lax | Cookie sent on same-site requests and safe top-level navigations (GET links). Default in modern browsers. Good balance. |
None | Cookie sent on all requests. Requires Secure. Needed for cross-site embeds (iframes, third-party widgets). |
Most apps should use Lax. Use Strict for highly sensitive admin interfaces. Use None only when you genuinely need cross-site cookies.
Domain and Path
These limit which requests get the cookie attached.
Set-Cookie: session=abc123; Domain=example.com; Path=/apiDomain=example.com includes all subdomains (app.example.com, api.example.com). If you omit Domain, the cookie is scoped to the exact host that set it only.
Path=/api means the cookie is only sent with requests to paths that start with /api. Requests to / or /about will not include it.
Expiry
# Expires at a specific date
Set-Cookie: prefs=dark; Expires=Wed, 01 Jan 2026 00:00:00 GMT
# Expires after N seconds from now
Set-Cookie: prefs=dark; Max-Age=86400
# Session cookie (deleted when browser closes, no expiry set)
Set-Cookie: temp=xyzPrefer Max-Age over Expires, it is relative to the current time so it does not depend on the client's clock being accurate.
Reading and writing cookies in JavaScript
The JavaScript cookieWhat is cookie?A small piece of data the browser stores and automatically sends with every request to the matching server, often used for sessions. APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. is notoriously awkward. You read and write through a single document.cookie string that behaves differently for reads and writes.
// Write a cookie
document.cookie = 'theme=dark; path=/; max-age=86400; samesite=lax';
// Read ALL cookies (one string, all concatenated)
console.log(document.cookie); // 'theme=dark; user=alice'
// Parse a specific cookie by name
function getCookie(name) {
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : null;
}
// Delete a cookie (set it with a past expiry)
document.cookie = 'theme=; max-age=0; path=/';js-cookie for client-side cookie management. For HttpOnly auth cookies, you never touch them from JavaScript at all, the browser handles them automatically.Cookies vs localStorage for authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token.
This is one of the most debated questions in frontend security. Here is the practical answer:
| Concern | HttpOnly cookie | localStorage |
|---|---|---|
| XSS resistant | Yes, JS cannot read it | No, any script can steal it |
| CSRF resistant | Only with SameSite set correctly | Yes, not sent automatically |
| Works across subdomains | Yes (Domain attribute) | No, per-origin |
| Mobile/native app friendly | Less convenient | More convenient |
For browser-based web apps, HttpOnlyWhat is httponly?A cookie flag that prevents JavaScript from reading the cookie. It stops XSS attacks from stealing session tokens or authentication cookies. cookies win on security. The tradeoff is that your server must set and manage them, and you need to configure SameSite correctly.
Quick reference
| Attribute | Purpose | Recommended value |
|---|---|---|
| HttpOnly | Blocks JS access | Always for session tokens |
| Secure | HTTPS only | Always for session tokens |
| SameSite | CSRF protection | Lax for most apps |
| Max-Age | Expiry in seconds | Based on session length |
| Path | URL scope | / unless you need to restrict |
| Domain | Subdomain scope | Omit unless you need subdomains |
// Secure session management using cookies
// ========== Server-side: setting cookies (Node.js / Express) ==========
// This is how secure session cookies should be set
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await validateCredentials(email, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const sessionToken = generateSecureToken(); // crypto.randomBytes(32).toString('hex')
await saveSession(sessionToken, user.id);
// HttpOnly so JavaScript cannot steal it
// Secure so it only travels over HTTPS
// SameSite=Lax for CSRF protection without breaking navigation
res.cookie('session', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
path: '/'
});
res.json({ user: { id: user.id, name: user.name } });
});
// ========== Client-side: reading non-sensitive cookies ==========
// Only read cookies that are NOT HttpOnly (e.g., UI preferences)
function getCookie(name) {
const match = document.cookie.match(
new RegExp(`(?:^|; )${encodeURIComponent(name)}=([^;]*)`)
);
return match ? decodeURIComponent(match[1]) : null;
}
function setCookie(name, value, options = {}) {
const {
maxAge = 86400,
path = '/',
sameSite = 'lax',
secure = location.protocol === 'https:'
} = options;
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
cookie += `; max-age=${maxAge}; path=${path}; samesite=${sameSite}`;
if (secure) cookie += '; secure';
document.cookie = cookie;
}
function deleteCookie(name) {
document.cookie = `${encodeURIComponent(name)}=; max-age=0; path=/`;
}
// Usage: store a non-sensitive UI preference in a cookie
setCookie('theme', 'dark', { maxAge: 86400 * 365 });
console.log(getCookie('theme')); // 'dark'