Storage and browser security

Learn how HTTP Cookies work

Learn how HTTP cookies move between browser and server, how scope and expiration work, and how Secure, HttpOnly, and SameSite protect them.

An HTTP cookie is a small name-value pair the browser stores for a site and sends back on later requests.

Their main job is identifying a session: the cookie holds an opaque session ID, the server keeps the account data. They are not a database.

How cookies travel

The server sets a cookie with a response header:

Set-Cookie: session=abc123; Path=/; Secure; HttpOnly; SameSite=Lax

On later matching requests the browser adds:

Cookie: session=abc123

“Matching” depends on domain, path, expiration, Secure, and SameSite. Your JavaScript never writes the Cookie header.

One cookie is capped at about 4 KB, and every matching request carries them all. For client-only data use Web Storage or IndexedDB.

Session and persistent cookies

Without Expires or Max-Age you get a session cookie:

Set-Cookie: theme=dark; Path=/

Careful: session restore can keep it across a restart.

Max-Age is a lifetime in seconds, here 30 days:

Set-Cookie: theme=dark; Max-Age=2592000; Path=/

Expires takes an HTTP date instead:

Set-Cookie: theme=dark; Expires=Tue, 18 Aug 2026 12:00:00 GMT; Path=/

With both present, Max-Age wins.

Omit Domain and you get a host-only cookie: set by app.example.com, sent only there. Add Domain when subdomains need it:

Set-Cookie: preference=compact; Domain=example.com; Path=/

My advice: leave Domain out unless you have a reason.

Path limits which paths receive the cookie. This matches /dashboard and below:

Set-Cookie: dashboard-view=list; Path=/dashboard

It’s a delivery rule, not a security boundary.

Protect cookies with attributes

Secure means HTTPS only. HttpOnly hides the cookie from document.cookie:

Set-Cookie: session=abc123; Secure; HttpOnly

Only the server can set HttpOnly. An XSS payload can’t read the cookie, but can still act as the user.

SameSite decides whether the cookie travels with cross-site requests. Strict is the narrowest, Lax also allows top-level navigations, None allows everything but requires Secure:

Set-Cookie: session=abc123; SameSite=Strict
Set-Cookie: session=abc123; SameSite=Lax
Set-Cookie: widget-session=abc123; SameSite=None; Secure

Browsers treat a missing value as Lax, with exceptions, so set it explicitly. It helps against CSRF but isn’t your only defense.

Partitioned gives a third-party cookie a separate jar per site:

Set-Cookie: widget=compact; SameSite=None; Secure; Partitioned

The __Host- prefix makes the browser enforce Secure, Path=/, and no Domain:

Set-Cookie: __Host-session=abc123; Path=/; Secure; HttpOnly; SameSite=Lax

Read and write cookies in JavaScript

document.cookie returns the non-HttpOnly cookies as one string:

console.log(document.cookie)
theme=dark; language=en

Assigning to it adds or updates one cookie:

document.cookie = 'theme=dark; Path=/; Max-Age=2592000; SameSite=Lax; Secure'

Encode values with special characters:

const value = encodeURIComponent('dark mode')
document.cookie = `theme=${value}; Path=/; SameSite=Lax; Secure`

The asynchronous Cookie Store API is nicer where supported.

Update by writing the same name, domain, and path:

document.cookie = 'theme=light; Path=/; Max-Age=2592000; SameSite=Lax; Secure'

Delete with the same scope and Max-Age=0:

document.cookie = 'theme=; Path=/; Max-Age=0; SameSite=Lax; Secure'

A mismatched path or domain touches a different cookie. That’s the usual “won’t delete” bug.

For session cookies: unpredictable IDs, Secure, HttpOnly, an explicit SameSite, the smallest scope, rotation after login, expiry on the server.

Never trust a cookie because the browser sent it. Users edit cookies and craft requests. Signing detects tampering, it doesn’t replace authorization.

More in MDN’s using HTTP cookies, the Set-Cookie reference, and document.cookie.

Lesson completed