告别隐藏字段:现代 CSRF 保护,无需 Token
Source: Dev.to
如果你曾经配置过针对跨站请求伪造(CSRF)攻击的防护,你可能还记得那套常规做法:生成唯一的令牌,将其嵌入隐藏的表单字段,并确保你的脚本发送正确的头部信息。在2025年,这个过程可以大幅简化,同时让你的代码更加简洁。
传统 CSRF 令牌
- 状态同步 – 令牌要求服务器和客户端保持共享状态,增加了页面缓存的复杂性。
- 标记膨胀 – 每个表单都需要额外的隐藏输入。
- 调试痛点 – 过期令牌错误会消耗宝贵的开发时间。
这些缺点使得基于令牌的防护感觉像是不可避免的技术债务。
基于浏览器的保护与 Fetch Metadata
现代浏览器现在会发送一种特殊的头部,称为 Fetch Metadata。最关键的元素是 Sec-Fetch-Site 头部,它告诉服务器请求的真实来源。
| 请求来源 | 头部值 |
|---|---|
| 同源表单提交 | Sec-Fetch-Site: same-origin |
| 跨站请求(例如恶意站点) | Sec-Fetch-Site: cross-site |
浏览器保证此头部不能被 JavaScript 伪造或修改,服务器可以完全依赖它。
旧版浏览器的回退方案
如果浏览器未发送 Fetch Metadata,你可以回退检查 Origin 或 Referer 头部。same-origin 或 none(例如直接输入 URL)的请求被允许;其他请求则被拒绝。
实现示例
以下是 Node.js(Express)和 Python(Flask)的最小示例,用于拒绝对状态更改方法的跨站请求。
Express(Node.js)
// app.js
const express = require('express');
const app = express();
function csrfProtection(req, res, next) {
const method = req.method.toUpperCase();
const unsafeMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];
if (!unsafeMethods.includes(method)) {
return next(); // Safe method, no check needed
}
const site = req.get('Sec-Fetch-Site');
if (site && site === 'cross-site') {
return res.status(403).send('Forbidden: CSRF protection');
}
// Fallback for older browsers
const origin = req.get('Origin') || req.get('Referer');
if (origin && !origin.includes(req.get('Host'))) {
return res.status(403).send('Forbidden: CSRF protection');
}
next();
}
app.use(express.json());
app.use(csrfProtection);
app.post('/api/data', (req, res) => {
// Handle state‑changing request
res.json({ status: 'success' });
});
app.listen(3000, () => console.log('Server running on port 3000'));
Flask(Python)
# app.py
from flask import Flask, request, abort, jsonify
app = Flask(__name__)
def csrf_protect():
unsafe_methods = {'POST', 'PUT', 'PATCH', 'DELETE'}
if request.method not in unsafe_methods:
return # Safe method
site = request.headers.get('Sec-Fetch-Site')
if site == 'cross-site':
abort(403, description='Forbidden: CSRF protection')
# Fallback for older browsers
origin = request.headers.get('Origin') or request.headers.get('Referer')
if origin and request.host not in origin:
abort(403, description='Forbidden: CSRF protection')
app.before_request(csrf_protect)
@app.route('/api/data', methods=['POST'])
def handle_data():
# Process the request
return jsonify(status='success')
if __name__ == '__main__':
app.run(port=5000)
基于 Header 的 CSRF 防护优势
- 无需生成或存储令牌 – 消除会话端状态并减少标记。
- 缓存友好 – 表单可以缓存而无需担心令牌过期。
- 调试更简便 – 错误仅表现为标准 HTTP 状态码。
- 性能提升 – 传输字节更少,服务器端处理更少。
行业认可
OWASP 项目已将 Fetch Metadata 视为经典 anti‑CSRF 令牌的可行替代方案,将其从实验状态提升为推荐实践。
结论
浏览器正变得越来越智能,能够自动处理常规的安全任务。通过将 CSRF 保护从隐藏字段和令牌转移到可靠的请求元数据,您可以在保持相同安全水平的同时,实现更简洁、更轻量的代码库。这标志着向更简洁、更易维护的网络迈出了一步。