4、CSRF防护:CSRF攻击原理、Token机制实现、SameSite Cookie属性配置

CSRF,全称叫跨站请求伪造。这玩意儿我早些年刚做后端时,觉得它就是个「理论漏洞」,直到有一次在内部渗透测试中,亲眼看着测试人员用一个简单的图片标签,就绕过了我们当时的所有权限校验……嗯,从那以后,我再也不敢小看它了。

4.1 CSRF攻击原理:到底是怎么发生的?

说白了,CSRF就是利用了你对某个网站的「登录态」,让你在不知情的情况下,替攻击者执行了恶意操作。

举个例子你就明白了:

  1. 你登录了银行网站A,浏览器里留下了A的Cookie。
  2. 你没退出A,又去访问了一个恶意网站B。
  3. 网站B里藏了个请求,比如 <img src="https://bank.com/transfer?to=attacker&amount=10000" />
  4. 浏览器带着你的Cookie,向银行A发起了转账请求。
  5. 银行A一看,Cookie有效,用户已登录,转账成功!

为什么会这样?因为浏览器在发送请求时,会自动带上目标站点的Cookie。服务器只认Cookie,不认请求是从哪里发出来的。这就是CSRF的核心问题——服务器无法区分请求是用户主动发起的,还是被恶意网站伪造的

关键点:CSRF攻击利用的是「身份认证」的漏洞,而不是「加密」的漏洞。你的HTTPS、密码强度再高,也防不住CSRF。

4.2 Token机制实现:最经典的防御方案

CSRF Token是目前最主流的防御手段。我个人习惯把它叫做「一次性暗号」——服务器给每个合法用户发一个随机字符串,每次提交表单时都必须带上这个暗号,服务器验证通过才放行。

4.2.1 基本原理

  • 服务器生成一个随机的Token,存入Session或Cookie中。
  • 在渲染表单时,把Token作为隐藏字段嵌入。
  • 用户提交表单时,Token随请求一起发送。
  • 服务器比对请求中的Token和Session中的Token是否一致。

攻击者无法获取到你的Token,所以伪造的请求自然就通不过验证。

4.2.2 Python实现示例(Flask)

import secrets
from flask import Flask, session, request, render_template_string

app = Flask(__name__)
app.secret_key = 'your-secret-key-here'

def generate_csrf_token():
    if '_csrf_token' not in session:
        session['_csrf_token'] = secrets.token_hex(32)
    return session['_csrf_token']

@app.before_request
def csrf_protect():
    if request.method == 'POST':
        token = session.get('_csrf_token')
        request_token = request.form.get('_csrf_token')
        if not token or token != request_token:
            return 'CSRF验证失败', 403

@app.route('/transfer', methods=['GET', 'POST'])
def transfer():
    if request.method == 'POST':
        # 执行转账逻辑
        return '转账成功'
    return render_template_string('''
        <form method="post">
            <input type="hidden" name="_csrf_token" value="{{ csrf_token }}">
            金额:<input type="text" name="amount">
            <button type="submit">转账</button>
        </form>
    ''', csrf_token=generate_csrf_token())

我的经验:Token一定要用密码学安全的随机数生成器,比如Python的secrets模块。别用random模块,那个是伪随机,理论上可预测。我在一个老项目里就见过用random.randint()生成Token的……嗯,后来全改了。

4.2.3 Django中的CSRF防护

如果你用Django,那更简单了。框架自带CSRF中间件,开箱即用:

# settings.py
MIDDLEWARE = [
    'django.middleware.csrf.CsrfViewMiddleware',  # 默认已启用
    # ...
]

# 模板中使用
<form method="post">
    {% csrf_token %}
    <input type="text" name="amount">
    <button type="submit">转账</button>
</form>

Django的CSRF Token是绑定用户Session的,而且每次刷新页面都会重新生成。不过要注意,如果你用了缓存或分布式Session,要确保Token的同步。

4.3 SameSite Cookie属性配置:现代浏览器的原生防御

SameSite是浏览器提供的一个原生安全机制。你想想看,如果浏览器自己就能判断「这个请求是不是跨站的」,那很多问题不就迎刃而解了吗?

4.3.1 三个取值

属性值 行为 适用场景
Strict 完全禁止跨站携带Cookie 银行、支付等敏感操作
Lax 允许部分安全请求(GET、导航)携带Cookie 大多数Web应用(默认值)
None 允许所有跨站请求携带Cookie(需配合Secure) 第三方嵌入场景(如支付回调)

4.3.2 Python中的配置方式

# Flask中设置SameSite
from flask import make_response

@app.route('/login', methods=['POST'])
def login():
    resp = make_response('登录成功')
    resp.set_cookie(
        'session_id',
        'your-session-value',
        samesite='Lax',      # 可选 Strict / Lax / None
        secure=True,          # 生产环境必须开启
        httponly=True         # 防止XSS窃取Cookie
    )
    return resp

# Django中设置(settings.py)
SESSION_COOKIE_SAMESITE = 'Lax'
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True

注意:如果你把SameSite设为None,必须同时设置Secure=True(即仅HTTPS下生效)。否则浏览器会直接拒绝这个Cookie。我曾经在调试一个第三方支付回调时踩过这个坑,排查了半天才发现是少了Secure标记。

4.4 综合防御策略:别只依赖一种方案

我个人建议,不要只依赖Token或SameSite中的某一个。安全是分层防御的,多一层就多一分保障。

  • 第一层:SameSite=Lax —— 浏览器原生拦截大部分跨站请求。
  • 第二层:CSRF Token —— 对关键操作(转账、改密、删除)强制校验。
  • 第三层:二次确认 —— 敏感操作要求用户输入密码或验证码。
  • 第四层:Referer/Origin校验 —— 检查请求来源是否合法。

我曾经在一个金融项目中,把SameSite设为Strict,同时所有POST请求都校验Token,转账操作还要输入短信验证码。虽然用户体验上多了一步,但安全团队审计时直接给了满分。

4.5 避坑指南

  • 不要用GET请求做状态修改操作。 这是最基本的原则,但很多新手会犯。GET请求可以被图片、脚本、iframe等任意触发。
  • Token不要放在URL参数中。 URL可能被记录在浏览器历史、服务器日志、Referer头里,容易泄露。
  • SameSite不是银弹。 老版本浏览器(比如Safari 12之前)不支持SameSite,所以Token机制还是得保留。
  • API接口也要防CSRF。 别以为只有表单需要,REST API同样可能被CSRF攻击。建议使用自定义Header(如X-CSRF-Token)来传递Token。

总结一下:CSRF防御的核心思路就是「让服务器能确认请求是用户主动发起的」。Token机制靠的是「只有用户页面才有的暗号」,SameSite靠的是「浏览器帮你拦截跨站请求」。两者结合,基本就能挡住99%的CSRF攻击了。