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=Strictfor sensitive cookies, or at leastLaxwhere 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=Noneis 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 ashttps://example.com.evilare rejected. Do not use this example's simple cookie comparison as the sole defense. Actual login and authorization checks are omitted.