Skip to content

深入理解CSRF攻击:从原理到防御的全方位解析

在Web安全的版图中,CSRF攻击就像一位"身份窃贼" ,它不直接破坏系统,却能悄无声息地盗用用户的合法身份执行恶意操作。相比XSS攻击的"明火执仗",CSRF更像是"暗度陈仓" ,其隐蔽性往往让开发者猝不及防。本文将从基础概念出发,逐步剖析CSRF的攻击原理,并提供切实可行的防御方案。

一、CSRF攻击的原理

1. 攻击定义

跨站请求伪造(Cross-Site Request Forgery,简称CSRF)是一种利用用户已认证的会话信息,在用户不知情的情况下发起非预期操作的攻击方式。简单来说,就是攻击者诱导已登录目标网站的用户,在第三方页面触发对目标网站的恶意请求,由于浏览器会自动携带用户的身份凭证(通常是Cookie),目标网站会误认为这是用户的主动操作并执行。

可以用一个生活场景类比:你在咖啡店用公共电脑登录了网上银行,查看余额后没有退出账户就离开了。后面的人趁机在这台电脑上打开了一个看似无害的网页,而这个网页悄悄向银行发送了转账请求。由于银行检测到你仍处于登录状态,就执行了这笔转账。这里的" 未退出的登录状态"就是CSRF攻击的关键,而"看似无害的网页"就是攻击载体。

2. 技术原理

CSRF攻击的实现依赖于Web开发中的两个默认机制,这也是其能够得逞的核心原因:

  • Cookie的自动携带机制:浏览器会自动将目标域名下的Cookie附加到该域名的所有请求中,无论请求来自哪个源页面
  • 基于Cookie的身份认证:绝大多数Web应用通过Cookie验证用户身份,且仅验证身份凭证的有效性,不验证请求的真实发起者

攻击者正是利用了这种"重凭证轻源头" 的认证方式。当用户登录网站A后,网站A的Cookie被存储在浏览器中;此时若用户访问攻击者控制的网站B,网站B可以构造指向网站A的请求(如表单提交、图片加载等);浏览器会自动携带网站A的Cookie发送请求,网站A的服务器验证Cookie有效后,便会执行该请求。

与XSS攻击的本质区别在于:XSS是通过注入恶意代码获取用户权限,而CSRF是直接利用用户已有的权限执行操作。

3. 攻击流程

我们以一个"修改用户邮箱"的场景为例,详细解析CSRF攻击的完整流程:

  1. 用户建立合法会话
    用户在目标网站(如https://example.com)输入账号密码登录,服务器验证通过后,生成会话ID并通过Set-Cookie头返回:

    http
    Set-Cookie: session_id=abc123456; path=/; domain=example.com

    浏览器将该Cookie存储,并在后续所有向example.com的请求中自动携带。

  2. 攻击者准备恶意页面
    攻击者构建一个恶意网站https://evil.com,其中包含指向目标网站的隐藏请求:

    html
    <!-- 恶意页面中的自动提交表单 -->
    <form id="csrfForm" action="https://example.com/api/change-email" method="POST">
      <input type="hidden" name="new_email" value="attacker@evil.com">
    </form>
    <script>
      // 页面加载后自动提交表单
      window.onload = function() {
        document.getElementById('csrfForm').submit();
      }
    </script>
  3. 诱导用户访问恶意页面
    攻击者通过邮件、社交软件等方式,诱骗已登录example.com的用户点击链接访问https://evil.com

  4. 浏览器发送伪造请求
    用户访问后,恶意页面自动提交表单,浏览器向example.com发送请求,并自动携带session_id=abc123456的Cookie:

    http
    POST /api/change-email HTTP/1.1
    Host: example.com
    Cookie: session_id=abc123456
    Content-Type: application/x-www-form-urlencoded
    
    new_email=attacker@evil.com
  5. 服务器执行恶意操作
    目标服务器验证session_id有效,确认用户已登录,且未检测到请求异常,于是执行修改邮箱操作,将用户邮箱改为攻击者控制的邮箱。

二、CSRF攻击的防御措施

防御CSRF攻击的核心思路是:在验证用户身份的同时,确保请求确实来自用户的主动意愿。以下是经过实践检验的有效防御方案:

1. Token验证(最可靠方案)

Token验证(又称CSRF Token)是目前防御CSRF最主流、最有效的方式。其核心思想是在请求中加入一个服务器生成的随机令牌,服务器通过验证令牌的有效性判断请求合法性。

实现原理:

  • 服务器为每个会话生成唯一的随机令牌(Token),并存储在服务器端(如Session中)
  • 客户端发起请求时必须携带该Token(通常放在表单字段或请求头中)
  • 服务器接收请求后,比对请求中的Token与服务器存储的Token是否一致,不一致则拒绝请求

代码实现(前后端示例):

后端(Node.js + Express)生成并验证Token:

javascript
const express = require('express');
const session = require('express-session');
const crypto = require('crypto');
const app = express();

// 配置Session存储
app.use(session({
    secret: 'secure-secret',
    resave: false,
    saveUninitialized: true
}));

// 生成CSRF Token并返回给客户端
app.get('/profile', (req, res) => {
    // 为当前会话生成随机Token
    const csrfToken = crypto.randomBytes(16).toString('hex');
    // 存储到Session
    req.session.csrfToken = csrfToken;
    // 渲染页面时将Token传递给前端
    res.render('profile', {csrfToken});
});

// 处理敏感操作的接口(带Token验证)
app.post('/change-password', (req, res) => {
    const {csrfToken, newPassword} = req.body;

    // 验证Token是否匹配
    if (!csrfToken || csrfToken !== req.session.csrfToken) {
        return res.status(403).json({error: 'CSRF验证失败'});
    }

    // Token验证通过,执行修改密码逻辑
    // ...

    res.json({success: true});
});

前端(HTML表单)携带Token:

html
<!-- 表单中包含隐藏的CSRF Token字段 -->
<form action="/change-password" method="POST">
    <input type="hidden" name="csrfToken" value="{{csrfToken}}">
    <label>新密码:</label>
    <input type="password" name="newPassword">
    <button type="submit">确认修改</button>
</form>

AJAX请求携带Token示例:

javascript
// 从页面元数据获取Token(也可从Cookie或响应头获取)
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;

// 发送AJAX请求时在请求头携带Token
fetch('/api/sensitive-action', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken  // 自定义请求头携带Token
    },
    body: JSON.stringify({data: '敏感操作数据'})
});

Token验证的关键在于Token的随机性时效性,攻击者无法预测或复用有效Token,从而有效阻止伪造请求。

2. Referer检查

Referer是HTTP请求头的一个字段,记录了当前请求的来源页面URL。通过检查Referer,服务器可以判断请求是否来自可信域名,从而拒绝来自第三方网站的伪造请求。

实现方式:

  • 对于敏感操作,验证Referer是否为当前网站域名或可信域名
  • 若Referer为空或来自非可信域名,则拒绝请求

代码示例(后端验证):

javascript
// Express中间件:检查Referer
const checkReferer = (req, res, next) => {
    const referer = req.get('Referer');
    const allowedDomains = ['example.com', 'api.example.com'];

    // 检查Referer是否存在且来自允许的域名
    if (!referer) {
        return res.status(403).json({error: 'Referer验证失败'});
    }

    // 解析Referer中的域名
    const refererUrl = new URL(referer);
    const isAllowed = allowedDomains.some(domain =>
        refererUrl.hostname.endsWith(domain)
    );

    if (isAllowed) {
        next();  // 验证通过,继续处理请求
    } else {
        res.status(403).json({error: '非法的请求来源'});
    }
};

// 对敏感接口应用Referer检查
app.post('/transfer-funds', checkReferer, (req, res) => {
    // 处理转账逻辑
});

局限性:

  • Referer可被浏览器插件或隐私模式禁用,可能导致误判
  • 部分代理服务器会修改Referer,影响验证准确性
  • 攻击者可能通过某些手段伪造Referer(虽然难度较大)

因此,Referer检查通常作为辅助防御手段,而非单独使用。

3. SameSite Cookie配置

SameSite是Cookie的一个属性(2019年标准化),用于限制Cookie在跨站请求中的发送行为,从根源上切断CSRF攻击的关键环节——Cookie的自动携带。

属性值说明:

  • SameSite=Strict:完全禁止跨站请求携带Cookie。只有在当前域名下的请求才会携带,从其他网站跳转过来的请求也不会携带。

    • 安全性最高,但可能影响用户体验(如从搜索引擎跳转至网站时需重新登录)
  • SameSite=Lax:允许部分跨站请求携带Cookie。仅当使用GET方法且是顶级导航(如点击链接跳转)时才携带,POST请求、iframe中的请求等不会携带。

    • 平衡了安全性和用户体验,是推荐的默认值
  • SameSite=None:允许跨站请求携带Cookie,但必须同时设置Secure属性(仅在HTTPS下有效)

配置示例:

http
// 服务器响应头设置(推荐使用Lax)
Set-Cookie: session_id=abc123; SameSite=Lax; HttpOnly; Secure; Path=/

在Node.js中设置:

javascript
// Express响应中设置Cookie
res.cookie('session_id', 'abc123', {
    sameSite: 'lax',  // 或 'strict'
    httpOnly: true,   // 防止JavaScript访问Cookie
    secure: true,     // 仅在HTTPS下传输
    path: '/'
});

现代浏览器(Chrome 51+、Firefox 60+、Edge 79+)均支持SameSite属性,无需前端修改即可生效,是防御CSRF的"零成本"方案。

4. 其他防御手段

  • 验证码/二次验证
    在执行敏感操作(如转账、修改密码)时,要求用户输入验证码或再次验证密码。由于攻击者无法绕过用户的手动操作,能有效阻止CSRF攻击。

    示例:

    html
    <form action="/transfer" method="POST">
      <input type="hidden" name="csrfToken" value="{{csrfToken}}">
      <input type="text" name="targetAccount" placeholder="目标账户">
      <input type="number" name="amount" placeholder="金额">
      <input type="text" name="captcha" placeholder="请输入验证码">
      <img src="/captcha" alt="验证码">
      <button type="submit">确认转账</button>
    </form>
  • 使用自定义请求头
    对于AJAX请求,可添加自定义请求头(如X-Requested-With: XMLHttpRequest ),服务器验证该头存在后才处理请求。由于浏览器的同源策略,第三方网站无法添加此类自定义头,从而区分合法请求与伪造请求。

  • 限制请求方法
    敏感操作仅允许POST方法(而非GET),并验证Content-Typeapplication/jsonapplication/x-www-form-urlencoded,减少通过 <img><link>等标签发起的GET型CSRF攻击。

总结

CSRF攻击的本质是"借势作案",利用用户的合法身份执行未授权操作。防御CSRF的核心在于验证请求的真实性 ,而非仅仅验证用户身份。在实际开发中,建议采用"多层防御"策略:

  1. 基础层:启用SameSite=Lax Cookie属性,从浏览器机制层面限制Cookie跨站发送
  2. 核心层:对所有敏感操作实施Token验证,确保请求来源可信
  3. 增强层:辅以Referer检查和自定义请求头验证,增加攻击难度
  4. 应急层:关键操作添加验证码或二次验证,作为最后一道防线

与XSS攻击相比,CSRF的防御更依赖服务器端的配置和验证逻辑。作为前端开发者,需理解CSRF的攻击原理,在与后端协作时确保正确传递验证信息(如CSRF Token),共同构建安全可靠的Web应用。记住:安全防御没有银弹,多层次防护才是王道。