15.13. Secure Cookies
Table of Contents
Why Cookie Security Matters
Cookies are tiny pieces of data that browsers store and send with every request to a website. They are often used to keep users logged in, remember preferences, or track sessions.
From a backend perspective, cookies are high‑value targets. If an attacker can:
- Read a sensitive cookie, they might hijack a user session.
- Modify a cookie, they might escalate privileges or bypass checks.
- Force a user’s browser to send a cookie in a malicious request, they might exploit CSRF.
So “secure cookies” is really about how you store, protect, and validate cookie data so that even if something goes wrong, attackers gain as little as possible.
Secure cookies are primarily about:
- Limiting where and when cookies are sent.
- Making cookie contents hard or impossible to tamper with.
- Avoiding storage of sensitive secrets directly in cookies.
In this chapter you will see the main flags, patterns, and anti‑patterns that make cookies safe in a real backend application.
Cookie Basics Recap
A cookie is a name=value pair with optional attributes. The server sets a cookie using the Set-Cookie header in an HTTP response, for example:
Set-Cookie: session_id=abc123; Path=/; HttpOnly; Secure; SameSite=LaxOn subsequent requests to the same site, the browser automatically sends:
Cookie: session_id=abc123Cookies are stored in the browser, not on the server. On the backend, you typically see only:
Cookieheader containing name/value pairs.- Possibly a session store that maps
session_idto server data.
You do not control how the browser stores them, but you control what to set and how to validate what comes back.
Sensitive Data and Cookies
What is “sensitive” in a cookie?
Examples of sensitive data that should not be easily exposed:
- Session identifiers, access tokens.
- User IDs if they can be used directly to access data.
- Email, personal profile data, or anything protected by privacy rules.
- Security tokens such as CSRF tokens or password reset tokens.
You will often still store some of these indirectly in cookies, but you must protect them properly.
Bad ideas: what not to store in plaintext cookies
Avoid storing:
- Passwords:
Set-Cookie: password=mysecret123 - Raw authentication tokens:
Set-Cookie: jwt=eyJhbGci...without any flags. - Unencrypted personal information:
Set-Cookie: user_email=john@example.com
If an attacker can run JavaScript on your page (XSS), any readable cookie becomes visible to them. If you do not properly restrict cookie sending, some cookies may also be sent in cross‑site requests.
Rule: Never store passwords or raw secrets in cookies, especially not in plaintext.
Instead, store random identifiers that are meaningful only on the server side, or use strongly protected tokens with correct flags.
Cookie Attributes and Security Flags
Several cookie attributes directly affect security. They are part of the Set-Cookie header.
Common attributes:
| Attribute | Purpose |
|---|---|
Domain | Which domains can receive the cookie |
Path | Which paths on the domain can receive it |
Expires | Absolute expiration date |
Max-Age | Lifetime in seconds |
Secure | Only send over HTTPS |
HttpOnly | Inaccessible to JavaScript |
SameSite | Restrict cross-site sending (CSRF related) |
You will use most of these together to get a secure cookie.
HttpOnly Flag
What HttpOnly does
HttpOnly tells the browser:
Do not make this cookie available to JavaScript (for example document.cookie).
The cookie is still sent automatically with HTTP requests, but any script running in the page cannot read or modify it.
Example:
Set-Cookie: session_id=abc123; Path=/; HttpOnlyWith this cookie, code like:
console.log(document.cookie);
will not show session_id in most modern browsers.
This is critical to limit the damage of Cross‑Site Scripting (XSS). If an attacker injects JavaScript into your site, they can often steal any non‑HttpOnly cookie. They cannot directly read a properly set HttpOnly cookie.
Rule: All cookies that relate to authentication or sessions must be set with HttpOnly.
What HttpOnly does not protect against
HttpOnly does not:
- Prevent the cookie from being sent to attackers’ servers if they trick the browser into sending requests (e.g. by submitting a form). That is a CSRF problem.
- Prevent attackers from using a stolen cookie if they somehow get it from network logs, browser export, or other means.
- Provide encryption. The value is still visible in HTTP headers (in dev tools, logs, etc.).
So you still need complementary protections, such as SameSite, CSRF tokens, and HTTPS.
Secure Flag
What Secure does
Secure tells the browser:
Send this cookie only over HTTPS connections.
Example:
Set-Cookie: session_id=abc123; Path=/; HttpOnly; Secure
If someone accesses http://example.com, the browser will not include the cookie. Only https://example.com will receive it.
This reduces the risk that cookies leak over unencrypted connections. Without Secure, if you ever serve on plain HTTP or if traffic is intercepted, the cookie could be read in transit.
Rule: Any authentication or session cookie must be set with Secure and only used over HTTPS.
Local development considerations
In local development you might use http://localhost:8000. With Secure cookies:
- Some browsers do not send them over plain HTTP.
- Workarounds:
- Use HTTPS locally (for example with a reverse proxy or dev certificate).
- Only for local development, consider making cookies non Secure, but always enable
Securein production.
Never ship to production with session cookies that lack Secure.
SameSite Flag
SameSite is essential to limit cross‑site requests carrying your cookies. It controls when the browser includes a cookie with a request coming from another site.
Common values:
| Value | Behavior summary |
|---|---|
Lax | Send cookie on top‑level navigations (such as clicking a link), but not on most cross‑site subrequests. |
Strict | Do not send cookie on any cross‑site request, including top‑level navigations. |
None | Send cookie on all cross‑site requests. Must be combined with Secure. |
Example:
Set-Cookie: session_id=abc123; Path=/; HttpOnly; Secure; SameSite=LaxWhy SameSite helps CSRF
Cross‑Site Request Forgery (CSRF) exploits the fact that browsers automatically send cookies in requests, even when the user is on another site.
SameSite helps by not sending cookies in many cross‑site cases. For example:
- If your banking app uses
SameSite=LaxorStrictfor session cookies, - A malicious site that auto submits a
POSTform to your bank will often not include the session cookie, - The bank will treat the request as unauthenticated.
This does not remove the need for CSRF tokens in all cases, but it is a powerful layer.
Rule: Use SameSite=Lax as a safe default for most authentication cookies. Use SameSite=None; Secure only when cross‑site usage is required.
Choosing the right SameSite value
Strict: Maximum protection, but can break flows:- Cookies are not sent when the user clicks a link from another site to yours.
- Good for very sensitive admin interfaces where convenience is less important.
Lax(recommended default):- Cookies are sent when the user manually navigates to your site from another site.
- Cookies are not sent for many cross‑site POSTs, images, iframes, etc.
- Balances usability and protection.
None:- Required when your app is embedded in third‑party contexts such as iframes,
or when you have a frontend onapp.example.comand an API onapi.example.comand they count as cross‑site in some browsers. - Must be combined with
Secure. Many browsers enforce this.
Example for cross‑site API cookie:
Set-Cookie: session_id=abc123; Path=/; HttpOnly; Secure; SameSite=NoneDomain and Path Restrictions
Domain attribute
Domain tells the browser which hostnames the cookie is valid for.
Examples:
Domain=example.com:- Cookie is sent to
example.comand all subdomains: www.example.comapi.example.com- If you do not specify
Domain, the cookie is usually restricted to the exact host that set it.
Example header:
Set-Cookie: session_id=abc123; Path=/; Domain=example.com; HttpOnly; Secure; SameSite=LaxSecurity implications:
- If you set
Domain=example.com, any subdomain can potentially access the cookie (depending on browser behavior and how requests are made). - If one subdomain is compromised, this can affect all cookies available to that domain.
Rule: Use the narrowest Domain you can. Do not share sensitive cookies across unnecessary subdomains.
Path attribute
Path limits cookies to specific URL paths.
Examples:
Set-Cookie: session_id=abc123; Path=/; ...Cookie is sent for:
//account/admin/settings
Another example:
Set-Cookie: admin_token=xyz; Path=/admin; ...Cookie is sent for:
/admin/admin/users/admin/settings- But not for
/or/api.
This can help limit where a cookie is visible and reduce attack surface.
Use case: You might want:
- A general session cookie for the whole app:
Path=/. - A special admin CSRF token cookie:
Path=/admin.
Expiration and Max-Age
Cookies can be:
- Session cookies: No
ExpiresorMax-Age. They are deleted when the browser session ends. - Persistent cookies: With
ExpiresorMax-Age. They survive browser restarts.
Examples:
Set-Cookie: session_id=abc123; Path=/; HttpOnly; Secure; SameSite=LaxThis is a session cookie.
Set-Cookie: remember_me=xyz; Path=/; HttpOnly; Secure; Max-Age=2592000; SameSite=Lax
This lasts 30 days (2592000 seconds) unless the user clears cookies.
Security notes:
- Shorter lifetimes limit the time window for attackers who obtain a cookie.
- Long‑lived “remember me” cookies should be treated like persistent login tokens:
- Use random, non guessable values.
- Store them in your database with a hashed or strong format.
- Allow users to revoke them.
Rule: Sensitive cookies, especially for powerful admin accounts, should have limited lifetimes and be rotated regularly.
Session Cookies vs Token Cookies
Two common patterns for authentication:
- Session ID cookie
- Cookie value is a random ID such as
"abc123"or a 32‑byte base64 string. - Server stores session data in a database or in Redis keyed by this ID.
- Cookie by itself has little information, server decides what it means.
- Token cookie
- Cookie value is a self‑contained token, often a JWT.
- Token contains user ID and claims, possibly signed.
- Server validates signature, then trusts claims without looking up session.
Security tradeoffs:
- Session ID cookies:
- Can be revoked by deleting server session.
- Simpler to invalidate if compromised.
- Cookie data is meaningless without server store.
- Token cookies:
- Token content may expose data if read by attackers or logs.
- Revocation is more complex, unless you maintain a blocklist.
- Must be implemented very carefully to avoid misuse.
For absolute beginners, a safe approach is:
Start with simple session ID cookies stored in a secure server‑side session store. Use HttpOnly, Secure, and SameSite=Lax as defaults.
Signed and Encrypted Cookies
Sometimes you want to store data in a cookie but prevent tampering. For example:
Set-Cookie: preferences={"theme":"dark"}; ...A malicious user could change it to:
preferences={"is_admin":true}If your backend trusts the cookie blindly, this is a vulnerability.
Two strategies:
Signed cookies (integrity)
You generate a cookie value that includes:
- The data,
- A cryptographic signature created using a server secret.
For example, the logical value might be:
{"user_id": 123, "role": "user", "sig": "a1b2c3..."}Only the server can compute the correct signature because it knows the secret key.
On each request:
- Read cookie.
- Extract data and signature.
- Recompute signature from data using secret.
- Compare signatures.
- If they match, data is trusted.
- If not, discard or reject.
If an attacker changes the data, the signature fails to verify.
Encrypted cookies (confidentiality + integrity)
You encrypt the content with a strong algorithm (for example AES) using a secret key.
- The browser sees only ciphertext, such as
g5N23k.... - On each request, the server:
- Decrypts the value.
- Checks integrity (for example with an authenticated encryption mode).
- Parses the data.
This protects both integrity and confidentiality. Even if someone reads the cookie, they cannot see what is inside.
In many modern frameworks, encrypted or signed cookies are built in. For example:
- Django: signed cookies for some features.
- Some frameworks: “encrypted cookie store” as a session backend.
Rule: If you store anything security relevant in cookies, either:
- Use a random session ID referencing server data, or
- Use signed/encrypted cookies managed by a well tested framework.
Never roll your own cryptography.
Preventing Session Hijacking
Session hijacking occurs when an attacker obtains a user’s session cookie and uses it.
Secure cookie practices help reduce this risk:
- Use
HttpOnlyto block simple XSS based theft. - Use
Secureto avoid sending cookies over plain HTTP. - Use
SameSiteto limit CSRF and some cross‑site leaks. - Rotate session IDs:
- When a user logs in, issue a new session ID and invalidate the old one.
- On privileged actions, consider “re‑authentication” or rotation.
- Short lifetimes for important sessions.
- Bind sessions to context:
- Optionally associate sessions with properties such as IP address or User‑Agent, then check for changes.
- Be careful, as this can affect users behind shared networks or using mobile networks.
Example pattern:
- User logs in successfully.
- Backend:
- Creates a new session entry in the session store.
- Generates a new random session ID.
- Sets cookie:
Set-Cookie: session_id=<random>; Path=/; HttpOnly; Secure; SameSite=Lax- Old session is deleted or marked invalid.
If a session cookie is stolen but you rotate frequently, the attacker has a shorter window of opportunity.
Preventing Session Fixation
Session fixation is when an attacker sets or forces a session ID for the victim, then later uses it after the victim logs in.
Typical unsafe flow:
- Attacker gets a valid session ID
A. - Attacker tricks user into using a link that sets
session_id=Ain some way. - User logs in while using session
A. - Attacker now uses session
Aand is authenticated as the user.
To prevent this:
Rule: Always generate a brand new session ID after a successful login (and after privilege changes).
Implementation idea:
- Before login, user can have an anonymous or guest session ID.
- After login:
- Create a new session with a new ID.
- Migrate any needed data from old session to new one.
- Delete or invalidate the old session.
- Send the new
Set-Cookieheader.
Example:
Set-Cookie: session_id=new_random_id; Path=/; HttpOnly; Secure; SameSite=LaxAfter this, any session ID the attacker forced earlier is no longer valid.
Cookies and CSRF Protection
Cookies and CSRF are closely related because:
- Browsers send cookies automatically on many requests.
- CSRF attacks exploit this automatic behavior.
You improve CSRF resistance by:
- Setting
SameSiteappropriately. - Implementing CSRF tokens where needed.
- Avoiding unnecessary cookie usage for cross‑site APIs (for example, using bearer tokens instead).
Example: SameSite=Lax plus CSRF token
SameSite=Laxmeans that cookies will not be sent in many cross‑site POST scenarios.- A CSRF token stored in a non cookie location, such as an HTML meta tag or in a separate HttpOnly cookie plus server side double submit pattern, helps you verify the source of a request.
A “double submit cookie” pattern:
- Server sets a CSRF cookie:
Set-Cookie: csrf_token=xyz; Path=/; Secure; SameSite=Lax- Frontend reads the
csrf_tokencookie (this one is not HttpOnly) and includes it in a custom headerX-CSRF-Tokenon each request. - Server:
- Checks that the cookie value and the header value match.
- If not, rejects the request.
Attackers from other sites typically cannot set the correct header and cannot read the cookie value due to cross origin restrictions.
Practical Examples and Patterns
Example: Secure login session cookie
Assume domain: app.example.com, HTTPS only.
On login:
HTTP/1.1 200 OK
Set-Cookie: session_id=RANDOM_32_BYTE; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600
Content-Type: application/json
{"message": "logged in"}Backend:
- Stores
session_idas a key in Redis: session:RANDOM_32_BYTE -> { "user_id": 42, "roles": ["user"] }- On each request:
- Reads
Cookie: session_id=.... - Looks up the session in Redis.
- Attaches user info to request context.
- If not found, returns 401.
This is simple and secure enough for many apps.
Example: Remember me cookie
You might want “remember me” that persists after 1 hour. Use a separate cookie:
Set-Cookie: remember_me=TOKEN; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000
Where TOKEN is a random identifier stored in your database, hashed:
| Field | Value |
|---|---|
| token_hash | hash(TOKEN) |
| user_id | 42 |
| created_at | 2026-08-20 |
| expires_at | 2026-09-19 (30 days later) |
On new visits:
- If no
session_idbutremember_meexists: - Verify token by hashing and checking database.
- If valid and not expired, log the user in automatically and generate a new session ID.
- Optionally rotate the remember me token too.
This way, even if someone dumps database, they do not see the raw token.
Common Mistakes with Cookies
Here are some patterns to avoid:
| Mistake | Why it is dangerous |
|---|---|
No HttpOnly on session cookies | XSS can steal session identifiers. |
No Secure on session cookies | Cookies may be intercepted over HTTP. |
SameSite=None without real need | Increases CSRF risk unnecessarily. |
Using Domain=.example.com for all cookies | All subdomains share powerful cookies. |
| Storing passwords or secrets in plaintext | Exposure from XSS, logs, browser export, etc. |
| No session rotation on login | Vulnerable to session fixation attacks. |
| Overly long Max-Age for powerful sessions | Longer window for attackers to use stolen cookies. |
| Implementing custom crypto incorrectly | Hard to get right, often leads to broken security. |
Summary
Secure cookie handling combines multiple techniques:
- Use
HttpOnlyto protect against JavaScript based theft. - Use
Secureand HTTPS to protect cookies in transit. - Use
SameSiteto reduce CSRF risk. - Scope cookies with
DomainandPath, following least privilege. - Use appropriate
ExpiresorMax-Ageto limit lifetime. - Prefer random session IDs with server side session storage.
- If storing data in cookies, use framework supported signed or encrypted cookies.
- Rotate session IDs on login and critical changes.
- Combine cookies with CSRF protections and input validation.
Cookies are not “just storage”. They are part of your authentication and security model. Treat every sensitive cookie like a key to the user’s account and protect it with all the tools available.
These principles apply regardless of programming language or framework, and you will use them repeatedly in real backend systems.
Views: 8
KAHIBARO