在传统 Web 安全实践中,CSRF(跨站请求伪造)防护一直依赖于反 CSRF 令牌和隐藏表单字段。这种方法虽然有效,但带来了显著的工程复杂度:需要生成、存储、验证令牌,管理会话状态,处理表单提交时的令牌嵌入。随着现代浏览器标准的演进,一种基于Sec-Fetch-Site请求头的无令牌 CSRF 防护方案逐渐成熟,被 OWASP 列为 token-based 方法的完整替代方案。
Sec-Fetch-Site 机制的工作原理
Sec-Fetch-Site是 Fetch Metadata 请求头家族的一员,自 2023 年 3 月起,所有现代桌面和移动浏览器都会自动在请求中包含此头。该头有四个可能值:
same-origin:请求来自与目标服务器完全相同的源(协议、主机、端口一致)same-site:请求来自同一站点但不同子域名cross-site:请求来自完全不同的站点none:请求由用户直接发起(如地址栏输入、书签打开)
关键的安全保证在于,Sec-Fetch-Site是一个 "禁止请求头"(forbidden request header),这意味着无法通过 JavaScript 修改其值。服务器可以信任:如果该头存在,客户端是浏览器;如果值为cross-site,请求确实来自跨站上下文。
核心防护逻辑的实现
最基本的防护逻辑极其简单:拒绝所有Sec-Fetch-Site: cross-site的请求。在 Python 的 Microdot 框架中,Miguel Grinberg 的实现展示了这一思路:
def csrf_protect(request):
sec_fetch_site = request.headers.get('Sec-Fetch-Site')
if sec_fetch_site == 'cross-site':
return abort(403, 'CSRF protection: cross-site request rejected')
# 继续处理same-origin/same-site/none请求
return process_request(request)
然而,实际工程中需要考虑更多细节:
1. 子域名策略配置
same-site请求可能来自不信任的子域名。例如,api.example.com可能不希望接受来自marketing.example.com的请求。需要提供配置选项:
class CSRFProtection:
def __init__(self, allow_subdomains=False):
self.allow_subdomains = allow_subdomains
def validate(self, request):
sec_fetch_site = request.headers.get('Sec-Fetch-Site')
if sec_fetch_site == 'cross-site':
return False
if sec_fetch_site == 'same-site' and not self.allow_subdomains:
return False
return True
安全最佳实践是默认拒绝same-site请求,除非明确配置信任关系。
2. 旧浏览器兼容性处理
虽然大多数浏览器在 2019-2021 年间就实现了Sec-Fetch-Site,但 Safari 直到 2023 年才添加支持。对于不支持该头的旧浏览器,需要后备方案。
最常用的后备方案是检查Origin头。Origin头同样无法通过 JavaScript 修改,且支持时间更长(Firefox 桌面版 2019 年,Edge 和 Firefox 移动版 2020 年)。实现时需要处理两个技术难点:
难点一:Origin 与 Host 的比较复杂性
Origin头包含协议、主机和端口(如https://api.example.com:443),而Host头只包含主机和端口(如api.example.com:443)。更复杂的是,反向代理通常会修改Host头。
解决方案有两种:
- 显式配置允许的源列表(推荐)
- 解析和规范化比较逻辑
def validate_origin_fallback(request, allowed_origins):
origin = request.headers.get('Origin')
if not origin:
# 没有Origin头,可能是旧浏览器或直接请求
return False
# 检查是否在允许列表中
return origin in allowed_origins
难点二:Origin 头的存在性
并非所有请求都包含Origin头。根据规范,同源请求、GET 请求、HEAD 请求可能不包含Origin。需要结合其他验证机制。
3. 完整验证流程
一个健壮的验证流程应该包含多层检查:
def validate_csrf(request, config):
# 第一层:Sec-Fetch-Site检查
sec_fetch_site = request.headers.get('Sec-Fetch-Site')
if sec_fetch_site:
if sec_fetch_site == 'cross-site':
return False, 'cross-site request'
if sec_fetch_site == 'same-site' and not config.allow_subdomains:
return False, 'same-site request from untrusted subdomain'
# Sec-Fetch-Site验证通过
return True, None
# 第二层:Origin后备检查(针对不支持Sec-Fetch-Site的浏览器)
origin = request.headers.get('Origin')
if origin:
if origin not in config.allowed_origins:
return False, 'origin not in allowed list'
return True, None
# 第三层:其他验证(如Referer检查或降级到令牌验证)
if config.fallback_to_tokens:
return validate_csrf_token(request)
# 默认拒绝:既无Sec-Fetch-Site也无Origin
return False, 'missing security headers'
工程部署的监控指标
部署无令牌 CSRF 防护后,需要建立监控体系来评估效果和发现问题:
1. 请求头存在率监控
# 监控Sec-Fetch-Site头的存在率
def monitor_header_presence(request):
has_sec_fetch_site = 'Sec-Fetch-Site' in request.headers
has_origin = 'Origin' in request.headers
# 发送到监控系统
metrics.increment('csrf.headers.sec_fetch_site', int(has_sec_fetch_site))
metrics.increment('csrf.headers.origin', int(has_origin))
# 计算支持现代标准的比例
if has_sec_fetch_site:
metrics.increment('csrf.modern_browser')
elif has_origin:
metrics.increment('csrf.legacy_browser')
else:
metrics.increment('csrf.no_headers')
2. 拒绝请求分类统计
按拒绝原因分类统计,帮助识别问题模式:
cross-site_rejected:跨站请求被拒绝(正常防护)same-site_rejected:同站点但不同子域名请求被拒绝no_headers_rejected:无安全头请求被拒绝origin_mismatch:Origin 头不匹配
3. 用户影响评估
监控被拒绝请求的用户代理分布,评估对特定用户群体的影响:
def analyze_user_impact(rejected_requests):
user_agents = {}
for req in rejected_requests:
ua = req.headers.get('User-Agent', 'unknown')
browser = parse_browser(ua)
user_agents[browser] = user_agents.get(browser, 0) + 1
# 如果旧版本Safari用户被大量拒绝,可能需要调整策略
if user_agents.get('Safari < 16.4', 0) > threshold:
alert('High rejection rate for old Safari users')
与传统令牌方法的对比
优势
- 无状态性:不需要服务器端存储令牌状态,简化了分布式系统架构
- 性能优势:避免了令牌生成、验证的加密操作
- 开发体验:前端无需处理令牌嵌入,后端无需管理令牌生命周期
- 安全性:基于浏览器强制实施的机制,而非应用层逻辑
劣势
- 浏览器依赖:完全依赖浏览器实现,旧浏览器用户可能受影响
- 配置复杂性:需要正确处理子域名策略和 Origin 后备
- 监控要求:需要建立更完善的监控来发现兼容性问题
迁移建议
对于现有系统,建议采用渐进式迁移策略:
阶段一:并行验证 在现有令牌验证基础上添加 Sec-Fetch-Site 检查,记录但不拒绝请求:
def hybrid_validation(request):
# 传统令牌验证
token_valid = validate_csrf_token(request)
# 新机制验证
sec_fetch_valid = validate_sec_fetch_site(request)
# 记录验证结果对比
if token_valid != sec_fetch_valid:
log_discrepancy(request, token_valid, sec_fetch_valid)
# 暂时仍依赖令牌验证
return token_valid
阶段二:监控分析 运行 1-2 周,分析:
- 两种方法的一致性比例
- 被新机制拒绝的请求特征
- 用户影响范围
阶段三:逐步切换 根据监控数据,逐步将权重从令牌验证转移到 Sec-Fetch-Site 验证:
def weighted_validation(request, token_weight=0.7, header_weight=0.3):
token_score = validate_csrf_token(request) * token_weight
header_score = validate_sec_fetch_site(request) * header_weight
# 逐步调整权重
return (token_score + header_score) >= 0.5
阶段四:完全切换 当监控显示新机制稳定且影响可控时,完全切换到 Sec-Fetch-Site 验证,保留令牌验证作为紧急回退机制。
特殊场景处理
1. API 客户端请求
非浏览器客户端(如移动应用、命令行工具)不会发送 Sec-Fetch-Site 头。需要为 API 请求提供替代验证机制:
def validate_api_request(request):
# API请求通常使用API密钥或OAuth令牌
api_key = request.headers.get('X-API-Key')
if api_key and validate_api_key(api_key):
return True
# 或者检查Bearer令牌
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
return validate_oauth_token(auth_header[7:])
return False
2. 文件上传请求
文件上传可能使用multipart/form-data格式,需要确保验证逻辑正确处理:
def validate_file_upload(request):
# 检查Content-Type
content_type = request.headers.get('Content-Type', '')
if content_type.startswith('multipart/form-data'):
# 对于文件上传,仍然可以检查Sec-Fetch-Site
return validate_sec_fetch_site(request)
return False
3. WebSocket 连接
WebSocket 握手请求也包含 Sec-Fetch-Site 头,可以在握手阶段进行验证:
async def websocket_handler(request):
# 在WebSocket握手时验证
if not validate_sec_fetch_site(request):
await request.respond(403, 'CSRF protection')
return
# 握手通过,建立连接
ws = await request.accept()
# ... 处理WebSocket消息
安全边界与防御深度
虽然 Sec-Fetch-Site 提供了强大的 CSRF 防护,但仍应作为深度防御策略的一部分:
- 同源策略:确保敏感操作要求 same-origin 请求
- CORS 配置:正确配置跨域资源共享策略
- 内容安全策略:实施 CSP 防止 XSS 攻击
- 会话管理:使用 Secure、HttpOnly、SameSite cookies
特别是 SameSite cookie 属性与 Sec-Fetch-Site 形成互补防御:
SameSite=Strict:浏览器不会在跨站请求中发送 cookieSameSite=Lax:允许顶级导航的跨站请求携带 cookie- 结合 Sec-Fetch-Site 验证,提供双重保护
结论
基于 Sec-Fetch-Site 的无令牌 CSRF 防护代表了 Web 安全实践的现代化演进。它利用浏览器内置的安全机制,简化了开发复杂度,提高了防护的可靠性。然而,成功部署需要:
- 细致的工程实现:正确处理子域名策略、旧浏览器兼容性
- 完善的监控体系:实时跟踪防护效果和用户影响
- 渐进式迁移策略:从传统令牌方法平滑过渡
- 深度防御思维:结合其他安全机制形成多层防护
随着浏览器标准的进一步普及,这种无令牌方法有望成为 CSRF 防护的新标准。对于新项目,建议直接采用;对于现有系统,建议通过渐进式迁移来平衡安全性与兼容性。
资料来源
- Miguel Grinberg, "CSRF Protection without Tokens or Hidden Form Fields" - 详细介绍了在 Microdot 框架中的实现
- MDN Web Docs, "Sec-Fetch-Site header" - 官方技术规范和浏览器兼容性信息
- OWASP, "Cross-Site Request Forgery Prevention Cheat Sheet" - 将 Fetch Metadata 方法列为 token-based 方案的替代方案