← Recipes

阅读中文版本

Verify SameSite Cookie Behavior Across Browsers

Set deliberately misconfigured SameSite cookies in a test page, observe which requests carry them, and prove the hardening actually blocks cross-site sends.

~30 minsecuritycookiescsrf

Task

Demonstrate with evidence which cookie flavors are sent on same-site, cross-site subresource, and top-level navigations, then enforce the strict one.

When to use this

The login cookie is set with no SameSite attribute at all. Nobody knows whether browsers default it to Lax or treat it as None, and CSRF reports keep arriving.

Prerequisites

  • Two test hostnames or ports (site-a.test, site-b.test)
  • A browser with devtools and a way to clear cookies between rounds
  • A backend that echoes received cookies

Steps

1. Plant three cookies with different SameSite modes on site A.

// served from site-a.test:8001/set
const express = require('express');
const app = express();
app.get('/set', (req, res) => {
  res.cookie('k_strict', '1', { sameSite: 'strict', httpOnly: true });
  res.cookie('k_lax', '1', { sameSite: 'lax', httpOnly: true });
  res.cookie('k_none', '1', { sameSite: 'none', secure: true });
  res.send('cookies planted');
});
app.get('/echo', (req, res) => res.json({ got: Object.keys(req.cookies || {}) }));
app.listen(8001);

Expected output: Visiting /set plants three cookies; a same-site /echo request lists all three keys, proving the baseline works before cross-site tests.

2. Request a site-A subresource from site B and see which cookies travel.

<!-- served from site-b.test:8002/leak -->
<img src="https://site-a.test:8001/echo?probe=img">
<iframe src="https://site-a.test:8001/echo?probe=iframe"></iframe>
<script>fetch('https://site-a.test:8001/echo?probe=fetch', { credentials: 'include' });</script>

Expected output: Server logs for the three probes show only k_none arriving: strict and lax cookies are withheld from cross-site subresources.

3. Test top-level navigation, where Lax is famously permissive.

curl -s "https://site-a.test:8001/echo?probe=nav" -H "Cookie: k_strict=1; k_lax=1" | jq .

Expected output: The echo shows k_lax on top-level GET navigations from B to A, while k_strict stays behind: this is exactly why Lax is the safe default and Strict breaks deep links.

4. Harden the real session cookie and verify the withholding.

// before: no SameSite -> browser-dependent default
res.cookie('session', token, { httpOnly: true, secure: true });

// after: explicit, with the fallback for older browsers
res.cookie('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
});

Expected output: Re-running the cross-site probe shows the session cookie absent from subresource requests, while same-site navigation keeps working.

5. Confirm CSRF-style POSTs no longer carry the cookie cross-site.

<form action="https://site-a.test:8001/transfer" method="POST">
  <input name="to" value="attacker">
</form>
<script>document.forms[0].submit();</script>

Expected output: The auto-submitted cross-site POST arrives with no session cookie, the server rejects it as unauthenticated, and the CSRF class is closed for this vector.

Troubleshooting

Problem

Set-Cookie with SameSite=None is dropped by the browser

Solution

None requires the Secure attribute and HTTPS; serve the probe page over TLS or use a local certificate before testing the None case.

Problem

Cookies behave differently across browsers

Solution

That is the finding, not a bug: run the matrix on the browsers you actually support and record per-browser results, defaulting to Lax which all modern engines honor.

Problem

Strict breaks returning users from email links

Solution

Top-level navigation with Strict withholds the cookie, logging users out; keep session cookies Lax and reserve Strict for short-lived anti-CSRF tokens on sensitive pages.

Limitations

  • SameSite is CSRF mitigation, not authentication; pair it with CSRF tokens for state-changing POST endpoints.
  • Older embedded browsers ignore SameSite entirely; capability-detect or require token-based APIs where those clients matter.

SameSite is an experiment you can run tonight

Cookie behavior differs per SameSite mode and per request shape, which makes it a perfect black-box experiment: plant three cookies, probe from a second origin, and read which ones arrive. The results justify the hardening choice instead of folklore.

Request shape                 | strict | lax | none
------------------------------|--------|-----|-----
same-site fetch               |   Y    |  Y  |  Y
cross-site <img> subresource  |   -    |  -  |  Y
cross-site iframe             |   -    |  -  |  Y
cross-site fetch +credentials |   -    |  -  |  Y
top-level GET navigation      |   -    |  Y  |  Y
cross-site form POST          |   -    |  -  |  Y

Lax is the default because it is the balance

Lax withholds cookies on subresources and cross-site POSTs, killing the classic CSRF image-and-form vectors, while still sending them on top-level navigations so links in emails keep you logged in. Strict is safer but logs users out of link-followed sessions, which usually reads as a bug report.

None is a deliberate hole

Third-party cookie scenarios require None, which reopens every cross-site send. If you must use it, scope a separate cookie to that purpose, keep the main session Lax, and audit which endpoints accept the None cookie. Never upgrade the session cookie itself to None for convenience.