双重提交 Cookie(Double Submit Cookie)是一种 CSRF 防护技术,它通过在 Cookie 和请求参数中同时存储相同的 Token 来验证请求的合法性。
双重提交 Cookie 的基本原理
- Token 生成:服务器生成一个随机的 CSRF Token
- 双重存储:Token 同时存储在 Cookie 和请求参数中
- 验证逻辑:服务器验证 Cookie 中的 Token 和请求参数中的 Token 是否匹配
实现步骤
1. 生成 Token
javascriptfunction generateCSRFToken() { return crypto.randomBytes(32).toString('hex'); } // 中间件:生成并设置 Token function csrfTokenMiddleware(req, res, next) { const token = generateCSRFToken(); res.cookie('csrfToken', token, { httpOnly: false, // JavaScript 需要读取 secure: true, sameSite: 'strict' }); res.locals.csrfToken = token; next(); }
2. 在表单中包含 Token
html<form action="/api/submit" method="POST"> <input type="hidden" name="csrfToken" value="<%= csrfToken %>"> <!-- 其他表单字段 --> <button type="submit">提交</button> </form> <!-- 或者通过 JavaScript 设置 --> <script> const form = document.querySelector('form'); const csrfToken = document.querySelector('meta[name="csrf-token"]').content; const input = document.createElement('input'); input.type = 'hidden'; input.name = 'csrfToken'; input.value = csrfToken; form.appendChild(input); </script>
3. 验证 Token
javascriptfunction validateDoubleSubmitCookie(req) { const cookieToken = req.cookies.csrfToken; const paramToken = req.body.csrfToken || req.query.csrfToken; if (!cookieToken || !paramToken) { return false; } // 使用恒定时间比较防止时序攻击 return crypto.timingSafeEqual( Buffer.from(cookieToken), Buffer.from(paramToken) ); } // 验证中间件 function csrfProtection(req, res, next) { if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') { return next(); } if (!validateDoubleSubmitCookie(req)) { return res.status(403).send('CSRF token validation failed'); } next(); }
工作原理
为什么双重提交有效?
- 同源策略:恶意网站无法读取目标网站的 Cookie
- 跨站请求限制:恶意网站无法在请求参数中包含正确的 Token
- 匹配验证:只有同源请求才能同时访问 Cookie 和设置请求参数
攻击场景分析
html<!-- 恶意网站尝试发起 CSRF 攻击 --> <form action="https://example.com/api/transfer" method="POST"> <input type="hidden" name="to" value="attacker"> <input type="hidden" name="amount" value="1000"> <!-- 无法获取正确的 csrfToken --> </form> <script> document.querySelector('form').submit(); </script>
- 恶意网站可以发起请求
- 浏览器会自动发送 Cookie 中的 Token
- 但恶意网站无法在请求参数中包含正确的 Token
- 服务器验证失败,拒绝请求
优势
- 无需服务器状态:不需要在服务器端存储 Token
- 易于实现:实现相对简单
- 可扩展性:适合分布式系统
- 性能好:不需要查询数据库或 Session
局限性
-
Cookie 安全性:
- 如果 Cookie 被窃取(XSS),防护失效
- 需要配合 HttpOnly 使用(但 JavaScript 无法读取)
-
子域名风险:
- 如果子域名存在 XSS 漏洞,可能影响主域名
- 需要谨慎设置 Cookie 的域属性
-
Token 泄露:
- 如果 Token 在 URL 中暴露,可能被记录在日志中
- 应该使用 POST 请求传递 Token
最佳实践
1. 结合其他防护措施
javascriptapp.use(helmet()); // XSS 防护 app.use(cookieSession({ secret: 'secret', cookie: { httpOnly: true, secure: true, sameSite: 'strict' } })); app.use(csrfTokenMiddleware); app.use(csrfProtection);
2. Token 刷新策略
javascript// 每次请求后刷新 Token function refreshTokenMiddleware(req, res, next) { if (req.method !== 'GET' && req.method !== 'HEAD') { const newToken = generateCSRFToken(); res.cookie('csrfToken', newToken, { httpOnly: false, secure: true, sameSite: 'strict' }); res.locals.csrfToken = newToken; } next(); }
3. 安全配置
javascript// Cookie 配置 res.cookie('csrfToken', token, { httpOnly: false, // 允许 JavaScript 读取 secure: true, // 仅 HTTPS sameSite: 'strict', // 最严格的同站策略 maxAge: 3600000, // 1小时过期 domain: '.example.com' // 谨慎设置域 });
与 CSRF Token 的对比
| 特性 | 双重提交 Cookie | 传统 CSRF Token |
|---|---|---|
| 服务器状态 | 无需 | 需要 Session |
| 实现复杂度 | 简单 | 中等 |
| 分布式支持 | 优秀 | 需要共享 Session |
| 安全性 | 良好 | 优秀 |
| 性能 | 优秀 | 良好 |
双重提交 Cookie 是一种有效的 CSRF 防护技术,特别适合分布式系统和需要高性能的场景。但应该与其他安全措施配合使用,提供全面的安全保护。