SameSite=None cookies and CSRF

SameSite=None cookie

Description

Setting SameSite=None on a sensitive cookie allows it on cross-site requests where the browser's other cookie policies permit. Without separate CSRF defenses, an attacker may lure a user to a malicious page that sends requests with their authentication cookie. State changes such as password updates or transfers may then run with the user's privileges.

Potential impact

  • Cross-site requests may include authentication cookies and enable unwanted actions.
  • Password changes, payments, transfers, or profile updates may occur without consent.
  • An attacker may abuse the account to send spam, change settings, or create, modify, or delete data.

Remediation

  • Prefer SameSite=Strict for sensitive cookies, or at least Lax where the flow requires it.
  • When cross-site cookies are necessary, such as for third-party embeds or SSO, add CSRF defenses:
    • Use server-validated, session-bound CSRF tokens or a signed double-submit cookie pattern.
    • Compare Origin/Referer with the exact trusted origin for state-changing requests.
    • Use POST, PUT, or DELETE rather than GET for state changes, and validate tokens or origins on the server.
  • In Express, configure res.cookie(..., { sameSite: 'strict' | 'lax', secure: true, httpOnly: true }).
  • If SameSite=None is necessary, pair it with Secure and the CSRF defenses above.

Examples

Before

javascript
const crypto = require('crypto');
const express = require('express');
const app = express();

// Before: SameSite=None on the authentication cookie, without CSRF defenses.
app.post('/login', (req, res) => {
  const sessionId = crypto.randomBytes(32).toString('hex');
  res.cookie('auth', sessionId, {
    httpOnly: true,
    secure: true,
    sameSite: 'none' // Permits cookies on cross-site requests.
  });
  res.send('logged in');
});

app.listen(3000);

After

javascript
const express = require('express');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');

const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());

// Issue a supplementary token; use with the exact Origin/Referer check below.
app.get('/form', (req, res) => {
  const token = crypto.randomBytes(16).toString('hex');
  const sessionId = crypto.randomBytes(32).toString('hex');
  res.cookie('csrfToken', token, { httpOnly: true, secure: true, sameSite: 'strict' });
  res.cookie('session', sessionId, { httpOnly: true, secure: true, sameSite: 'strict' });
  res.send(`<form method="POST" action="/transfer">
              <input type="hidden" name="csrf" value="${token}">
              <button type="submit">send</button>
            </form>`);
});

function verifyOrigin(req) {
  const value = req.headers.origin || req.headers.referer;
  if (typeof value !== 'string') return false;

  try {
    return new URL(value).origin === 'https://example.com';
  } catch {
    return false;
  }
}

// After: SameSite=Strict, exact origin validation, and token comparison.
app.post('/transfer', (req, res) => {
  if (!verifyOrigin(req)) return res.status(403).send('forbidden');
  const cookieToken = req.cookies.csrfToken;
  const formToken = req.body?.csrf;
  if (typeof cookieToken !== 'string' || !/^[0-9a-f]{32}$/.test(cookieToken) ||
      typeof formToken !== 'string' || formToken !== cookieToken) {
    return res.status(403).send('bad token');
  }
  res.send('ok');
});

app.listen(3000);

Explanation:

  • Before: The authentication cookie permits cross-site use. Without token or origin checks, requests from a malicious page may cause unauthorized state changes.
  • After: Use unpredictable session values and SameSite=Strict. Parse Origin/Referer and compare the exact origin, then require matching tokens in the expected nonempty format. Prefixes such as https://example.com.evil are rejected. Do not use this example's simple cookie comparison as the sole defense. Actual login and authorization checks are omitted.

References