AARON.
/BLOG/Day 4: Cookies
Back to Blog
SecurityCookies

by Aaron Adetunmbi

Day 4: Cookies

Learn about cookie security best practices to protect your web applications from attacks.

Day 4: Session Cookies and CSRF Attacks

The Problem We're Solving

Before we talk about solutions, you need to understand the attack.

Your browser has a fundamental trust model: if you're logged into your bank, and you visit a malicious website in another tab, your browser will automatically send your bank's session cookie to any request that website makes to your bank.

This is by design. It's how websites remember who you are. But it's also how attackers trick you into doing things you didn't intend to do.

This attack is called CSRF: Cross-Site Request Forgery.

How CSRF Actually Works

Let's say you're logged into your bank at bank.com. You have a session cookie in your browser that proves you're authenticated.

Now you visit a malicious website at evil.com (maybe through a phishing link or a legitimate site that got hacked).

The attacker's webpage contains this hidden code:

<img src="https://bank.com/api/transfer?to=attacker&amount=10000" />

When your browser loads this page, it automatically sends your bank's session cookie along with that request. Your bank receives it, sees a valid session cookie, and transfers $10,000 to the attacker.

You never clicked anything. You didn't know it happened. The attacker just used your authenticated session against you.

Why This Matters

CSRF attacks work because:

  1. Your browser automatically sends cookies to ANY request to that domain, regardless of where the request came from.
  2. The attacker doesn't need your password. They just need you to visit their page while you're logged in.
  3. The bank can't tell the difference between a legitimate request you made and a malicious one the attacker forced.

This is a real attack. It happens. Companies have lost millions because of CSRF vulnerabilities.

The Real-World Example: How Linus Tech Tips Got Hacked

In 2023, an employee at Linus Media Group downloaded a PDF file. The file contained malware that extracted their YouTube session cookie.

The attacker now had a valid session token. They didn't need the employee's password. They didn't need to bypass 2FA because 2FA only validates during login—once a session is established and you have a valid session cookie, you're authenticated. 2FA isn't re-checked on every request.

They just replayed the cookie.

With that cookie, they logged into YouTube as if they were the employee. They:

  • Accessed the channel admin panel
  • Livestreamed a cryptocurrency scam (making thousands in minutes)
  • Deleted years of videos
  • Renamed the channel

The damage was done in hours. Recovery took weeks.

Why did this happen? The session cookie had no protection. It was readable by malware. It was sent over any channel. It never expired. It was the master key, and once stolen, it unlocked everything.

The Solution: SameSite Cookies

The modern fix for CSRF is the SameSite attribute on cookies. It controls whether a cookie is sent on cross-site requests.

There are three values:

Browser default: If you don't set SameSite, modern browsers treat the cookie as Lax by default. This is good news—it means you get CSRF protection by default, even if you forget to set it.

SameSite=Strict

The cookie is ONLY sent when you're on the same website where you logged in.

res.cookie('sessionId', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'Strict',
  maxAge: 3600000  // 1 hour
});

What this prevents: The attacker's malicious webpage cannot trigger a request to your bank. The cookie won't be sent. CSRF is impossible.

Trade-off: If you click a link in an email that goes to your bank, the cookie won't be sent on the first request. You'll be logged out temporarily and need to log back in. Worse UX.

Use this for: High-security operations like payments, account changes, admin panels.

SameSite=Lax

The cookie is sent on same-site requests AND on top-level navigation (like clicking a link or a form submission that you explicitly triggered).

res.cookie('sessionId', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'Lax',
  maxAge: 7 * 24 * 60 * 60 * 1000  // 7 days
});

What this prevents: The attacker's malicious webpage cannot trigger a request to your bank using <img> or AJAX calls. If they try to submit a form on their malicious site to transfer money from your bank, the cookie won't be sent because it's not a top-level navigation YOU initiated.

What it allows: If you click a legitimate link in an email that goes to your bank, the cookie IS sent because you explicitly navigated there. If the bank redirects you to a login form via GET, the cookie is sent.

Important caveat: When a cookie is first set, there's a 2-minute grace period where Lax cookies ARE sent with POST requests even without top-level navigation. This helps with login flows. After 2 minutes, the protection kicks in fully. This means Lax has a brief vulnerability window right after login.

Use this for: General authentication. Standard sessions where you need email links and password reset flows to work. The UX is better than Strict, and CSRF is blocked for the vast majority of attacks.

SameSite=None

The cookie is sent on ALL requests, including cross-site.

res.cookie('sessionId', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'None',
  maxAge: 3600000
});

What this does: The attacker's malicious webpage CAN trigger a request to your bank. The cookie WILL be sent. CSRF attacks work perfectly.

When you'd use this: Almost never. Only if your API is explicitly designed to be called from third-party websites with credentials. This is extremely rare and requires you to have other defenses.

Real example of why this is dangerous: Langflow AI (2025) made this mistake. They set sameSite: 'None' on their refresh token cookie AND had overly permissive CORS. An attacker crafted a malicious webpage. When a Langflow user visited it, their browser sent the refresh token cookie automatically. The attacker stole it, gained account access, and executed arbitrary code. CVSS 9.4 (critical).

The Other Piece: httpOnly

sameSite solves CSRF. But there's another attack: XSS (Cross-Site Scripting).

If an attacker can inject JavaScript into your page (through a vulnerable comment field, user input, etc.), they can steal your cookies if they're readable by JavaScript.

The solution: httpOnly: true

res.cookie('sessionId', token, {
  httpOnly: true,    // JavaScript cannot read this cookie
  secure: true,
  sameSite: 'Strict',
  maxAge: 3600000
});

With httpOnly: true:

  • The cookie is automatically sent on requests (the browser handles it)
  • JavaScript cannot access it (even your own code, even malicious code injected via XSS)
  • Malware on the user's device cannot read it (though it might still intercept network traffic if HTTPS isn't enforced)

The Complete Defense

Here's what a properly secured session cookie looks like:

res.cookie('sessionId', token, {
  httpOnly: true,      // Protects against XSS attacks
  secure: true,        // Only sent over HTTPS
  sameSite: 'Lax',     // Protects against CSRF, allows email links
  maxAge: 3600000,     // 1 hour expiry
  path: '/',
  domain: 'yourdomain.com'
});

Breaking it down:

  • httpOnly: true: Malware can't steal it via JavaScript
  • secure: true: Only sent over HTTPS, not HTTP (no man-in-the-middle interception)
  • sameSite: 'Lax': Attackers can't trigger cross-site requests with it (has a 2-minute grace period after setting)
  • maxAge: 3600000: Even if stolen, it expires in 1 hour
  • path: '/': Available to your entire application
  • domain: 'yourdomain.com': Sent to this domain AND its subdomains (like api.yourdomain.com). Omit this to scope it to the exact domain only

Each flag solves a different attack. Together, they form a complete defense.

What This Prevents

  • XSS attacks: Malicious JavaScript can't read the cookie
  • CSRF attacks: Malicious websites can't use the cookie to make unwanted requests
  • Man-in-the-middle attacks: The cookie is only sent over HTTPS
  • Session replay attacks: The cookie expires, making stolen old cookies useless
  • Subdomain attacks: The cookie is scoped to your exact domain.

When to Use What


Use sameSite: 'Strict' for:

  • Admin panels
  • Payment pages
  • Account settings
  • Sensitive operations

Use sameSite: 'Lax' for:

  • General authentication
  • Login flows that include email links
  • Standard web applications

Use sameSite: 'None' for:

  • Third-party APIs (rare, requires careful consideration)
  • Embedded content (chat widgets, payments embedded on other sites)

On Logout: Invalidate Immediately

When a user logs out, don't just clear the cookie. Invalidate the session server-side immediately:

app.post('/logout', (req, res) => {
  // Delete the session from your session store (Redis, database, etc.)
  if (req.session) {
    req.session.destroy((err) => {
      if (err) console.error('Session destruction error:', err);
    });
  }
  
  // Clear the cookie
  res.clearCookie('sessionId', {
    httpOnly: true,
    secure: true,
    sameSite: 'Lax',
    path: '/'
  });
  
  res.json({ success: true });
});

Why both? Because a cookie is just a key. Even if you clear it from the browser, if the session still exists on the server, someone with a copy of that session token can use it. Invalidate server-side first.


Key Takeaway: Session cookies are powerful. They're also dangerous if misconfigured. sameSite, httpOnly, secure, and short expiry times work together to defend against CSRF, XSS, interception, and session replay attacks. Get all of them right, and your sessions are locked down.

Next: We'll talk about CORS and controlling which domains can access your API in the first place.