0x55aa
← Back to Blog

Cookie Security Flags: The Five Attributes Hackers Hope You Forget πŸͺ

β€’8 min read

Cookie Security Flags: The Five Attributes Hackers Hope You Forget πŸͺ

Here's a fun story. Early in my career I shipped a Laravel app to production, and a pentest came back with this finding: "Session cookie missing HttpOnly flag." I thought, "That sounds minor." Then I read the description. An attacker with any XSS foothold could steal every logged-in user's session with a single line of JavaScript.

Minor, sure. 😬

Cookies are the keys to your users' front doors. Yet most tutorials teach you to Set-Cookie: session=abc123 and move on. Nobody explains the five attributes that turn that key into a key with a deadbolt, a chain lock, and a "beware of dog" sign.

Let's fix that.

Why Cookie Security Matters (The Five-Second Horror Story) 😱

When a user logs in, your server gives their browser a cookie. That cookie proves who they are on every future request. If an attacker steals it, they become that user β€” no password needed.

The scary part? Stealing cookies is embarrassingly easy when security flags are missing.

1. HttpOnly: "JavaScript, Stay Out" 🚫

What it does: Stops JavaScript from reading the cookie at all.

Without HttpOnly:

// Any XSS payload can do this
document.cookie  // β†’ "session=super_secret_token_123"
// Attacker sends this to their server. Game over.

With HttpOnly:

document.cookie  // β†’ "" (the session cookie is completely invisible)

In PHP / Laravel:

// Bad: no HttpOnly
setcookie('session', $token);

// Good: HttpOnly enabled
setcookie('session', $token, [
    'httponly' => true,
]);

// Laravel's session config (config/session.php)
'http_only' => true,  // It's already true by default β€” don't turn it off!

In Node.js / Express:

// Bad
res.cookie('session', token);

// Good
res.cookie('session', token, { httpOnly: true });

Real Talk πŸ’¬: This single flag makes XSS attacks 10x harder to monetize. Even if an attacker injects JavaScript onto your page, they can't steal sessions. In my experience building production systems, I've seen teams accidentally set http_only => false in Laravel's config when "debugging" β€” and then forget to revert it. Don't be that team.

2. Secure: "HTTPS Only, Please" πŸ”’

What it does: The browser will only send the cookie over encrypted HTTPS connections. Never over plain HTTP.

Without Secure, if a user is on public WiFi and accidentally hits an HTTP version of your site (redirect misconfiguration, mixed content, etc.), their cookie travels in plaintext. Anyone sniffing traffic on that network β€” in security communities, we often discuss how trivially easy this is with tools like Wireshark β€” can capture it.

// Laravel config/session.php
'secure' => env('SESSION_SECURE_COOKIE', true),
// Express
res.cookie('session', token, {
    httpOnly: true,
    secure: true,   // Only over HTTPS
});

Pro Tip 🎯: Set secure: true in production, false in local dev (since localhost is often HTTP). Use environment variables to control this β€” exactly what SESSION_SECURE_COOKIE is for in Laravel.

3. SameSite: The CSRF Killer 🎯

What it does: Controls whether the browser sends cookies on cross-site requests. This is your built-in CSRF protection.

There are three values:

Value Behavior
Strict Cookie never sent on cross-site requests
Lax Cookie sent on top-level navigations only (clicking a link)
None Cookie sent everywhere (requires Secure flag)

The real-world translation:

SameSite=Strict: Evil hacker site POSTs to your bank? No cookie sent. Attack fails.
SameSite=Lax: Evil site auto-POSTs? No cookie. User clicks a link to your site? Cookie sent.
SameSite=None; Secure: Cookie always sent β€” needed for third-party widgets, OAuth flows.
// Laravel config/session.php
'same_site' => 'lax',  // Good default
// Express
res.cookie('session', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'Lax',
});

In my experience building production systems β€” particularly e-commerce platforms with embedded payment forms β€” getting SameSite wrong caused us real headaches. We set Strict globally and broke our payment provider's OAuth redirect flow. Users would authenticate with Stripe, get redirected back, and be mysteriously logged out. Lax is the sweet spot for most applications.

4. Domain: Scope It Right πŸ—ΊοΈ

What it does: Tells the browser which domains should receive the cookie.

This one trips up developers building multi-subdomain apps:

# Without Domain set:
# Cookie only goes to api.yourapp.com (exact match)

# With Domain=.yourapp.com (note the dot):
# Cookie goes to api.yourapp.com, admin.yourapp.com, anything.yourapp.com

The trap: If you set Domain=.yourapp.com to share a session across subdomains, and one of those subdomains gets compromised (XSS, subdomain takeover), the attacker can steal sessions from your entire platform. As someone passionate about security β€” and someone who's done subdomain enumeration in bug bounty programs β€” I can tell you that wide domain scopes are a goldmine for attackers.

Rule of thumb: Scope cookies to the minimum domain they actually need.

5. Expires / Max-Age: Don't Let Sessions Live Forever ⏰

What it does: Sets when the cookie dies.

A session cookie with no expiry lives until the browser closes. Sounds fine β€” until you realize most users never fully close browsers. They hibernate laptops, keep tabs open for weeks.

// Laravel config/session.php
'lifetime' => 120,  // Minutes β€” pick something reasonable
'expire_on_close' => false,  // True means session dies when browser closes
// Express β€” 2 hours session
res.cookie('session', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'Lax',
    maxAge: 2 * 60 * 60 * 1000,  // 2 hours in milliseconds
});

As someone passionate about security, I'd argue that session expiry is the most overlooked flag. Stolen credentials age out. Compromised devices stop being a permanent liability. A bank I once reviewed had session cookies with 30-day lifetimes and no idle timeout. One stolen laptop = one month of unlimited account access.

The Complete Secure Cookie πŸ†

Putting it all together:

// Node.js β€” the full secure cookie setup
res.cookie('session', token, {
    httpOnly: true,           // No JavaScript access
    secure: true,             // HTTPS only
    sameSite: 'Lax',          // CSRF protection
    domain: 'yourapp.com',    // Exact domain (no subdomain sharing unless needed)
    maxAge: 7200000,          // 2 hours
    path: '/',                // Cookie valid for all paths
});
// PHP β€” the equivalent
setcookie('session', $token, [
    'expires'  => time() + 7200,
    'path'     => '/',
    'domain'   => 'yourapp.com',
    'secure'   => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

And in Laravel, most of this is already configured in config/session.php β€” you just need to check the values instead of assuming defaults are production-safe.

Quick Security Audit: Check Your Cookies Right Now πŸ”

Open your browser DevTools (F12) β†’ Application β†’ Cookies. Look at your session cookie:

  • βœ… HttpOnly column = checked?
  • βœ… Secure column = checked?
  • βœ… SameSite = Lax or Strict?
  • βœ… Expiry = reasonable (not "Session" for weeks, not years into the future)?

If any of these are wrong β€” you've found a real vulnerability in production. Go fix it before someone else does.

Real Talk: But My Framework Handles This! πŸ’¬

Partially true. Laravel's session driver sets HttpOnly and SameSite=Lax by default. Express does basically nothing by default β€” you're on your own.

The danger zone is custom cookies. JWT refresh tokens, remember-me tokens, feature flags, A/B test assignments β€” developers hand-roll these all the time without thinking about flags. I've seen JWT refresh tokens stored in plain, JavaScript-readable cookies in multiple codebases. Every single one was vulnerable to XSS-based session takeover.

In security communities, we often discuss how compound vulnerabilities work: a low-severity XSS becomes a critical account takeover specifically because someone forgot HttpOnly on the refresh token cookie. The XSS alone might only get you a CVSS 4.0. Add the missing cookie flag, and suddenly you're escalating to 8.5.

Your Cookie Security Checklist πŸ“‹

Before you ship:

  • Session cookie has HttpOnly set
  • Session cookie has Secure set (and it works in production)
  • SameSite is Lax or Strict (not None unless you have a specific reason)
  • Cookie Domain is as narrow as possible
  • Session has a sensible expiry time
  • All custom cookies (JWT, remember-me, etc.) have the same flags as session cookies
  • Run your app through browser DevTools to verify before going live

TL;DR 🎯

Five cookie attributes. Five minutes to configure them. They stop XSS session theft (HttpOnly), man-in-the-middle attacks (Secure), CSRF (SameSite), subdomain scope creep (Domain), and indefinite session persistence (Max-Age).

Most frameworks set some of these for session cookies. None of them protect your custom cookies automatically. Check them all β€” every cookie you set.

The attacker sitting on the coffee shop WiFi next to your user is hoping you skipped this post.


Found a misconfigured cookie in your app? Fixed something after reading this? I'd love to hear about it β€” connect on LinkedIn or check out my projects on GitHub. Security is a community sport, and we get better together. πŸ›‘οΈ