深入解析CSRF攻击:从攻击机制到多层次防护策略

250 阅读7分钟

CSRF

一、什么是CSRF

  1. CSRF(Cross-site request forgery)跨站请求伪造
    • 攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的

二、CSRF的攻击类型

  1. GET类型的CSRF

    • 用户信任http://bank.com网站,并且保存登录状态
    • 诱导用户点击图片<img src="http://bank.com/transfer?to=attacker&amount=1000">
    • http://bank.com网站误以为是用户自己发送的请求,并以用户的名义执行了攻击者自定义的操作,触发转账
  2. POST类型的CSRF

    • 构造隐藏表单,利用JavaScript自动提交表单到目标网站

       <form action="http://bank.example/withdraw" method=POST>
          <input type="hidden" name="account" value="xiaoming" />
          <input type="hidden" name="amount" value="10000" />
          <input type="hidden" name="for" value="hacker" />
      </form>
      <script> document.forms[0].submit(); </script> 
      
  3. 链接类型的CSRF

    • 在论坛中发布的图片中嵌入**恶意链接,**或者以广告的形式诱导用户中招

        <a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
        重磅消息!!
        <a/>
      

三、防护策略

  1. CSRF的特点与策略

    • 特点
      • CSRF(通常)发生在第三方域名。
      • CSRF攻击者不能获取到Cookie等信息,只是使用。
    • 对应策略
      • 阻止不明外域的访问
        • 同源检测
        • Samesite Cookie
      • 提交时要求附加本域才能获取的信息
        • CSRF Token
        • 双重Cookie验证
  2. 同源检测

    • 原理
      • 检查请求的 RefererOrigin 头是否来自合法域名

      • 注意:部分浏览器可能不发送 Referer(如隐私模式),需结合其他防御手段。

        // 后端
        app.post('/transfer', (req, res) => {
          const referer = req.get('Referer');
          const origin = req.get('Origin');
          const allowedDomains = ['https://your-legitimate-site.com'];
        
          if (!allowedDomains.includes(referer) && !allowedDomains.includes(origin)) {
            return res.status(403).send('Invalid request source');
          }
          // 处理合法请求
        });
        
  3. Samesite Cookie

    • Set-Cookie响应头新增Samesite属性,它用来标明这个 Cookie是个“同站 Cookie”,同站Cookie只能作为第一方Cookie,不能作为第三方Cookie。

    • 假如淘宝网站用来识别用户登录与否的 Cookie 被设置成了 Samesite=Strict,那么用户从百度搜索页面甚至天猫页面的链接点击进入淘宝后,淘宝都不会是登录状态,因为淘宝的服务器不会接受到那个Cookie,其它网站发起的对淘宝的任意请求都不会带上那个 Cookie。

    • Samesite 有以下属性值
      模式行为适用场景
      Strict完全禁止跨站请求携带 Cookie(包括用户点击其他站点的链接跳转)极高敏感操作(如支付、修改密码)
      Lax允许安全的跨站 GET 请求(如导航到目标站的链接)携带 Cookie大多数场景(浏览器默认行为)
      None允许所有跨站请求携带 Cookie,但必须同时设置 Secure 属性(仅HTTPS)需要跨站 Cookie 的第三方集成场景
      // 后端设置Cookie
      app.post('/api/login', (req, res) => {
          res.cookie('CSRF-TOKEN', crypto.randomBytes(32).toString('hex'), {
              secure: true,
              sameSite: 'Lax',
              httpOnly: false  // 允许前端读取
          });
          res.send('OK');
      });
      
      // 验证
      app.post('/api/transfer', (req, res) => {
          if (req.cookies['CSRF-TOKEN'] !== req.headers['x-csrf-token']) {
              return res.status(403).send('CSRF验证失败');
          }
          // 处理业务逻辑
      });
      
      // 前端Axios全局配置自动携带Token
      axios.interceptors.request.use(config => {
          const token = document.cookie.match(/CSRF-TOKEN=([^;]+)/)?.[1];
          config.headers['X-CSRF-TOKEN'] = token;
          return config;
      });
      
  4. CSRF Token

    • 原理
      • 服务器生成一个随机且唯一的 Token,嵌入表单或 HTTP 请求头中,提交时验证 Token 是否合法。

      • Token 需满足:每次会话或请求唯一、加密随机、不可预测。

        // 后端
        HttpServletRequest req = (HttpServletRequest)request; 
        HttpSession s = req.getSession(); 
         
        // 从 session 中得到 csrftoken 属性
        String sToken = (String)s.getAttribute(“csrftoken”); 
        if(sToken == null){ 
           // 产生新的 token 放入 session 中
           sToken = generateToken(); 
           s.setAttribute(“csrftoken”,sToken); 
           chain.doFilter(request, response); 
        } else{ 
           // 从 HTTP 头中取得 csrftoken 
           String xhrToken = req.getHeader(“csrftoken”); 
           // 从请求参数中取得 csrftoken 
           String pToken = req.getParameter(“csrftoken”); 
           if(sToken != null && xhrToken != null && sToken.equals(xhrToken)){ 
               chain.doFilter(request, response); 
           }else if(sToken != null && pToken != null && sToken.equals(pToken)){ 
               chain.doFilter(request, response); 
           }else{ 
               request.getRequestDispatcher(“error.jsp”).forward(request,response); 
           } 
        }
        
        <!-- 前端表单嵌入 --!>
        <!-- 后端渲染页面时注入 Token -->
        <form action="/transfer" method="POST">
          <input type="hidden" name="_csrf" value="{{ csrfToken }}">  <!-- 例如通过模板引擎注入 -->
          <input type="number" name="amount">
          <button>Submit</button>
        </form>
        
        // 前端AJAX请求
        // 从 Meta 标签或 Cookie 获取 Token(需后端配合)
        const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
        
        // 发送请求时携带 Token
        axios('/api/transfer', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': csrfToken  // 通过 HTTP 头传递
          },
          body: JSON.stringify({ amount: 100 })
        });
        
  5. 双重Cookie验证

    • 原理
      • 无状态验证:无需服务器存储Token,通过客户端自行携带两份Token副本。
      • 跨域限制:攻击者无法通过跨域请求同时篡改请求参数和Cookie。
      • 动态绑定:Token与用户会话或请求上下文动态绑定,增强安全性。
    • 流程
      • 生成Token
        • 用户登录时,服务器生成随机Token,写入Cookie(如CSRF-Token=abc123)。
      • 前端传递Token
        • 前端通过JavaScript读取Cookie中的Token,将其添加到请求参数或HTTP头中。
      • 服务器验证
        • 服务器比对请求参数/头中的TokenCookie中的Token是否一致。
    • 与CSRF的区别
      特性双重Cookie验证传统CSRF Token
      服务端状态管理无状态需存储Token(Session/DB/Redis)
      实现复杂度简单,无需存储逻辑需处理Token生成、存储、验证
      抗XSS能力弱(依赖前端读取Cookie)强(Token可设置为HttpOnly)
      跨域支持需处理CORS和Cookie跨域策略需处理Token传递方式
      // 后端设置Cookie并验证
      // server.js
      const express = require('express');
      const cookieParser = require('cookie-parser');
      const cors = require('cors');
      const crypto = require('crypto');
      
      const app = express();
      app.use(express.json());
      app.use(cookieParser());
      
      // 配置CORS(根据实际部署调整)
      app.use(cors({
        origin: 'http://localhost:8080', // Vue前端地址
        credentials: true // 允许携带Cookie
      }));
      
      // 登录接口:生成Token并设置Cookie
      app.post('/api/login', (req, res) => {
        const { username, password } = req.body;
        
        // 模拟用户验证
        if (username === 'admin' && password === '123456') {
          // 生成随机Token
          const csrfToken = crypto.randomBytes(32).toString('hex');
          
          // 设置Token到Cookie(允许前端读取)
          res.cookie('CSRF-TOKEN', csrfToken, {
            httpOnly: false,    // 必须允许前端JS读取
            secure: process.env.NODE_ENV === 'production',
            sameSite: 'Lax',    // 允许安全跨站请求
            maxAge: 86400000    // 24小时有效期
          });
          
          return res.json({ success: true });
        }
        res.status(401).json({ error: '认证失败' });
      });
      
      // 敏感操作接口:验证双重Cookie
      app.post('/api/transfer', (req, res) => {
        const cookieToken = req.cookies['CSRF-TOKEN'];
        const headerToken = req.headers['x-csrf-token'];
        
        if (!cookieToken || cookieToken !== headerToken) {
          return res.status(403).json({ error: 'CSRF验证失败' });
        }
        
        // 处理业务逻辑
        res.json({ success: true, amount: req.body.amount });
      });
      
      app.listen(3000, () => {
        console.log('Server running on http://localhost:3000');
      });
      
      // 前端配置Axios全局拦截器 
      import axios from 'axios';
      
      const instance = axios.create({
        baseURL: 'http://localhost:3000/api',
        withCredentials: true // 允许跨域携带Cookie
      });
      
      // 请求拦截器:自动添加Token
      instance.interceptors.request.use(config => {
        // 从Cookie中获取Token
        const csrfToken = document.cookie
          .split('; ')
          .find(row => row.startsWith('CSRF-TOKEN='))
          ?.split('=')[1];
        
        if (csrfToken) {
          config.headers['X-CSRF-TOKEN'] = csrfToken;
        }
        return config;
      });
      
      export default instance;
      
  6. 分布式校验

    • 原理
      • 分布式Token管理:统一生成、存储和验证Token,确保跨服务请求的合法性。
      • 动态同步机制:解决多节点间的Token状态同步问题。
      • 去中心化验证:减少对单点服务的依赖,提升系统可用性。
    • 中央认证服务(推荐)
      • 原理:由中心化服务统一生成Token,各服务节点通过API验证Token。
    • 使用Session存储CSRF Token会带来很大的压力,使用计算出来的Token,而非随机生成的字符串Token,这样在校验时无需再去读取存储的Token,只用再次计算一次即可(Encrypted Token Pattern方式)

  7. 总结

    • 基础防御组合:CSRF Token + SameSite Cookie
      • 适用场景
        • 传统多页面Web应用(如Django、Rails应用)
        • 混合型架构(部分服务端渲染 + 部分AJAX请求)
      • 实现方式
        • CSRF Token

          • 服务端生成随机Token,嵌入表单或HTTP头(如X-CSRF-TOKEN)。
          • 提交请求时验证Token合法性。
        • SameSite Cookie

          • 设置会话Cookie的SameSite=Lax,限制跨站请求自动携带Cookie。
          • 敏感操作Cookie设为SameSite=Strict(如支付确认)。
    • 增强防御组合:加密Token + 双重Cookie验证
      • 适用场景
        • 单页应用(SPA)(如Vue、React应用)
        • 无状态API服务(如RESTful接口)
      • 实现方式

        • 加密Token

          • 使用AES或HMAC加密Token,嵌入用户ID、时间戳等元数据。
          • 防止Token被窃取后直接重放。
        • 双重Cookie验证

          • 服务端设置CSRF-TOKEN Cookie(SameSite=None; Secure)。
          • 前端读取该Cookie并添加到请求头(如X-CSRF-Token)。
          • 服务端比对Cookie和请求头的Token是否一致。
    • 高安全组合:JWT + Origin验证 + SameSite Strict
      • 适用场景

        • 高敏感操作(如金融交易、密码修改)

        • 跨域API服务(需严格限制来源)

      • 实现方式

        • JWT(JSON Web Token):(详情可看前端鉴权之JWT-CSDN博客

          • 用户登录后签发JWT,包含用户身份和操作权限。
          • 请求时通过Authorization: Bearer <token>传递。
        • Origin验证

          • 检查请求头中的OriginReferer是否为合法域名。
        • SameSite Strict

          • 设置关键Cookie为SameSite=Strict,完全禁止跨站请求携带。