浏览器 Web 安全基础系列
CSRF 如何防御(当前篇)
没有一种单独措施适合所有系统。生产环境通常组合:安全 Cookie、CSRF Token、请求来源校验和正确的 HTTP 方法。每一层处理不同的绕过条件。

第一层:SameSite Cookie
普通登录 Session 可以使用:
Set-Cookie: __Host-session=...; Path=/; HttpOnly; Secure; SameSite=Lax
Lax 会阻止很多跨站表单 POST 携带 Cookie,同时允许用户通过普通链接进入网站。敏感后台可以评估 Strict;确实需要跨站 Cookie 时使用 None; Secure,但必须加强其他 CSRF 防御。
SameSite 不能覆盖同站恶意子域名。evil.example.com 与 bank.example.com 可能同站,所以不能只靠它。
第二层:同步 CSRF Token
服务端在 Session 中保存随机 Token:
import crypto from 'node:crypto';
function createCsrfToken() {
return crypto.randomBytes(32).toString('base64url');
}
sessions.set(sessionId, {
userId,
csrfToken: createCsrfToken()
});
传统表单将 Token 放进隐藏字段:
<input type="hidden" name="csrfToken" value="服务器生成的随机值">
前后端接口放进自定义请求头:
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ to: 'bob', amount: 100 })
});
服务端比较请求 Token 与 Session Token:
function verifyCsrf(session, requestToken) {
if (!session || !requestToken) return false;
const expected = Buffer.from(session.csrfToken);
const actual = Buffer.from(requestToken);
return expected.length === actual.length &&
crypto.timingSafeEqual(expected, actual);
}

攻击页面可以让浏览器自动发送 Cookie,但通常无法跨源读取正常页面中的随机 Token,也无法用普通表单设置自定义请求头。
Token 放在哪里
推荐:
传统 HTML 表单:隐藏字段;
Fetch/Ajax:
X-CSRF-Token请求头。
不建议放入 URL,因为 URL 会进入历史记录、访问日志、代理和 Referer。
只把 Token 放进 Cookie,然后服务端只检查 Cookie,没有意义:Cookie 在 CSRF 中同样会被自动携带。关键是请求必须主动提交第二份值。
Double Submit Cookie
无服务端 Session 的系统可以设置可由 JavaScript 读取的 CSRF Cookie,前端把值复制到请求头,服务端比较二者。更稳妥的实现应把 Token 与用户会话进行签名绑定,防止子域名注入 Cookie。
伪代码:
csrfCookie = random + '.' + HMAC(sessionId + random, secret)
// 请求时同时出现:
Cookie: csrf=...
X-CSRF-Token: ...
第三层:Origin 校验
正常请求:
Origin: https://bank.example
攻击请求:
Origin: https://evil.example
服务端对敏感方法精确匹配白名单:
const ALLOWED = new Set(['https://bank.example']);
function verifySource(req) {
const origin = req.headers.origin;
if (origin) return ALLOWED.has(origin);
const referer = req.headers.referer;
if (!referer) return false;
try {
return ALLOWED.has(new URL(referer).origin);
} catch {
return false;
}
}
不能使用 includes、模糊正则或简单 endsWith 判断域名。
Referer 为什么只是回退
Referer 可能包含完整页面 URL,受 Referrer-Policy、隐私设置和跳转协议影响,存在缺失情况。Origin 更简洁、更适合来源判断;没有 Origin 时再解析 Referer 的 Origin。
高风险接口如果两个请求头都缺失,通常采取拒绝策略。业务必须支持特殊客户端时,可以使用 CSRF Token 或明确的非浏览器认证机制兜底,而不是无条件放行。
哪些请求需要 CSRF 防护
重点保护所有会改变状态的 Cookie 认证接口:
POST / PUT / PATCH / DELETE
GET、HEAD 应保持无副作用。登录接口也可能遭受 Login CSRF:攻击者让受害者登录到攻击者账号,随后诱导受害者把隐私数据写进该账号。
推荐中间件结构
function csrfProtection(req, res, next) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
if (!verifySource(req)) return res.status(403).send('Invalid source');
if (!verifyCsrf(req.session, req.get('X-CSRF-Token') || req.body.csrfToken)) {
return res.status(403).send('Invalid CSRF token');
}
next();
}
实际项目可使用成熟框架能力,但必须知道其 Token 模式、Cookie 属性和适用的认证方式。
XSS 与 CSRF 防御
如果攻击者已经能在目标页面执行 JavaScript,他可能读取页面中的 Token、使用正常 Origin 发请求,并借浏览器自动携带 HttpOnly Cookie。因此 XSS 防御优先级非常高,CSRF Token 不能替代输出编码和 CSP。
常见误区
Token 越频繁更换越安全
每请求一次性 Token 会增加多标签页、后退、重试和并发复杂度。很多系统使用每 Session 一个高强度随机 Token即可。
校验 Referer 字符串前缀
https://bank.example.evil.com 可能绕过错误前缀判断。必须用 URL 解析并比较 .origin。
CORS 白名单等于 Origin 防御
CORS 决定响应可读性;Origin 校验决定业务是否执行。虽然都使用 Origin 请求头,但目的不同。
小结
推荐思路不是“选一个最强方案”,而是:Cookie 用 Secure + HttpOnly + SameSite,状态修改请求验证 CSRF Token 和来源,GET 不修改数据,并同时防御 XSS。
参考资料
CSRF 如何防御:Token、SameSite、Origin 与 Referer
https://lautung.com/archives/web-security-07-csrf-defense
评论