安全审查--跨站请求伪造--双重提交Cookie模式

安全Top10 https://cheatsheetseries.owasp.org/IndexTopTen.html

安全审查--跨站请求伪造--同步令牌模式

安全审查--跨站请求伪造--双重提交Cookie模式

安全审查--跨站请求伪造--Fetch Metadata防护模式

CSRF防护模式选择指南:三种方案的对比与决策


摘要:从小白开始逐层讲解双重提交Cookie模式Double-Submit Cookie Pattern

一、从一个真实的安全问题说起

1.1 一个令人困惑的现象

假设你开发了一个网上银行系统,用户登录后可以转账。有一天,用户小王向你反映一个奇怪的问题:

"我昨天只是浏览了一个搞笑网站,什么都没做,但今天登录银行时发现我的账户少了1000块钱!"

作为开发者,你查看日志发现确实有一笔转账记录,IP地址、用户认证都是正确的,但用户坚称自己没有进行过转账操作。

这到底是怎么回事呢?

1.2 背后的"隐形之手"

经过深入调查,你发现了真相:

用户登录网上银行 ──────┐

浏览恶意网站 │ ← 用户浏览器自动携带银行Cookie

(含有隐藏的表单) │

恶意网站向银行发送请求 ──┘ ← 浏览器自动发送银行Cookie

**这就是CSRF攻击!**攻击者利用了浏览器的"自动携带Cookie"特性,在用户不知情的情况下发送了恶意请求。

1.3 直观理解CSRF攻击

想象一下:

  1. 正常的转账过程:
  • 用户在银行网站填写转账表单

  • 点击"确认转账"按钮

  • 浏览器发送请求到银行服务器

  • 银行服务器看到Cookie,确认用户身份

  • 完成转账

  1. CSRF攻击过程:
  • 用户访问恶意网站(看起来完全无害)

  • 恶意网站自动构建一个转账请求

  • 浏览器自动携带用户的银行Cookie发送请求

  • 银行服务器同样看到Cookie,以为是用户操作

  • 完成转账(但用户完全不知情)

关键问题:银行服务器无法区分这个请求是用户主动发起的,还是被恶意网站诱导发起的。

二、双重提交Cookie模式的巧妙构思

2.1 灵感来源:同源策略的保护

聪明的开发者们想到了一个巧妙的方法:利用浏览器的同源策略。

什么是同源策略?

简单说,就是A网站的JavaScript无法读取B网站的Cookie。这是一个浏览器的重要安全机制。

为什么这个很重要?

  • 攻击者的网站(恶意网站)可以诱导浏览器向银行网站发送请求(浏览器会自动携带Cookie)

  • 但攻击者的JavaScript无法读取银行网站的Cookie内容

2.2 双重提交Cookie的核心理念

基本思路:让用户请求中携带两次相同的token,一次在Cookie中(浏览器自动发送),一次在请求头/请求体中(JavaScript需要主动添加)。

<!-- 恶意网站能做的事 -->

复制代码
  <script>
  // 发送请求到银行网站
  fetch('/api/transfer', {
    method: 'POST',
    body: JSON.stringify({ to: 'hacker', amount: 1000 })
    // 但是!攻击者无法获取银行的Cookie,所以无法添加正确的token
  });
  </script>

正常用户的浏览器能做的事:

复制代码
  // 用户的银行网站
  const csrfToken = getCookie('XSRF-TOKEN'); // 同源,可以读取Cookie

  fetch('/api/transfer', {
    method: 'POST',
    headers: {
      'X-XSRF-Token': csrfToken // JavaScript主动添加token
    },
    body: JSON.stringify({ to: 'friend', amount: 100 })
  });

2.3 形象化理解:双重要验证机制

把双重提交Cookie想象成一个双向验证系统:

银行系统 用户身份验证

│ 1. 你说你是张三?(请求头中的token)

│ ↑

│ │ 2. 你的身份证显示你是张三?(Cookie中的token)

│ │ ↑

│ │ │ 3. 两个信息一致吗?

│ │ │ ✓ 通过 - 正常用户

│ │ │ ✗ 失败 - 攻击者(只能提供身份证,无法说对名字)

│ │ │

处理请求 ←─┘ └─

攻击者为什么失败?

  • 攻击者的网站可以诱导浏览器发送请求(自动携带Cookie = 身份证)

  • 但攻击者的JavaScript无法读取Cookie内容(无法说对token = 名字)

  • 银行验证两个token不一致,拒绝请求

三、从零开始实现双重提交Cookie

3.1 第一步:设置CSRF Token

让我们从最简单的实现开始:

复制代码
  // 服务器端:当用户首次访问时设置token
  app.use((req, res, next) => {
    // 检查用户是否已经有CSRF token
    if (!req.cookies['XSRF-TOKEN']) {
      // 生成一个随机的token
      const token = crypto.randomBytes(32).toString('hex');

      // 设置到Cookie中(关键:httpOnly: false,让JavaScript能读取)
      res.cookie('XSRF-TOKEN', token, {
        httpOnly: false,  // 重要!
        secure: true,     // HTTPS必须
        sameSite: 'strict'
      });

      console.log('给用户设置了CSRF token:', token.substring(0, 8) + '...');
    }

    next();
  });

关键点解释:

  • httpOnly: false:这是关键!让JavaScript能够读取Cookie

  • secure: true:只在HTTPS下发送,提高安全性

  • sameSite: 'strict':防止跨站点请求

3.2 第二步:前端获取并使用Token

现在前端需要从Cookie中读取token,并在每次请求时携带:

复制代码
// 获取CSRF token的简单函数
  function getCsrfToken() {
    const name = 'XSRF-TOKEN=';
    const cookies = document.cookie.split(';');

    for (let cookie of cookies) {
      while (cookie.charAt(0) === ' ') {
        cookie = cookie.substring(1);
      }
      if (cookie.indexOf(name) === 0) {
        return cookie.substring(name.length);
      }
    }
    return null;
  }

  // 使用token发送请求
  async function transferMoney(to, amount) {
    const token = getCsrfToken(); // 从Cookie获取token

    const response = await fetch('/api/transfer', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-XSRF-Token': token // 在请求头中携带token
      },
      body: JSON.stringify({ to, amount })
    });

    return response.json();
  }

3.3 第三步:服务器端验证

服务器需要验证Cookie中的token和请求头中的token是否一致:

复制代码
// CSRF验证中间件
  const csrfProtection = (req, res, next) => {
    // GET请求通常是安全的,跳过验证
    if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
      return next();
    }

    // 从Cookie获取token
    const cookieToken = req.cookies['XSRF-TOKEN'];

    // 从请求头获取token
    const headerToken = req.headers['x-xsrf-token'];

    // 验证
    if (!cookieToken || !headerToken || cookieToken !== headerToken) {
      return res.status(403).json({
        error: 'CSRF验证失败',
        message: '请求被拒绝'
      });
    }

    next();
  };

  // 应用到需要保护的路由
  app.post('/api/transfer', csrfProtection, (req, res) => {
    // 处理转账逻辑
    res.json({ success: true });
  });

四、进阶实现:生产级双重提交Cookie

4.1 安全增强:恒定时间比较

上面的简单实现有一个安全漏洞:时序攻击。我们需要改进token比较方式:

复制代码
// 不安全的比较方式
  if (cookieToken !== headerToken) {
    // 拒绝请求
  }

  // 安全的比较方式:恒定时间比较
  function safeCompare(a, b) {
    if (a.length !== b.length) {
      return false;
    }

    let result = 0;
    for (let i = 0; i < a.length; i++) {
      result |= a.charCodeAt(i) ^ b.charCodeAt(i);
    }

    return result === 0;
  }

  // 使用安全比较
  if (!safeCompare(cookieToken, headerToken)) {
    return res.status(403).json({ error: 'CSRF验证失败' });
  }

为什么这样更安全?

  • 简单的!==比较在第一个字符不同时就会返回false

  • 攻击者可以通过响应时间差异推断token信息

  • 恒定时间比较确保无论输入如何,执行时间都相同

4.2 Token生命周期管理

生产环境中,我们需要管理token的生命周期:

复制代码
class CsrfTokenManager {
    static generateToken() {
      return crypto.randomBytes(32).toString('hex');
    }

    static setTokenCookie(res, token) {
      res.cookie('XSRF-TOKEN', token, {
        httpOnly: false,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'strict',
        maxAge: 24 * 60 * 60 * 1000, // 24小时后过期
        path: '/'
      });
    }

    static refreshTokenIfNeeded(req, res) {
      const currentToken = req.cookies['XSRF-TOKEN'];

      // 如果没有token,生成新的
      if (!currentToken) {
        const newToken = this.generateToken();
        this.setTokenCookie(res, newToken);
        return newToken;
      }

      // 检查token是否快要过期(可选)
      // 这里可以实现更复杂的刷新逻辑
      return currentToken;
    }
  }

4.3 前端自动化集成

为了让前端使用更方便,我们可以创建一个自动化系统:

复制代码
// CSRF管理器
  class CsrfManager {
    constructor() {
      this.token = null;
      this.init();
    }

    init() {
      // 页面加载时获取token
      this.token = this.getTokenFromCookie();

      // 自动为所有表单添加token
      this.injectTokenToForms();

      // 拦截fetch请求,自动添加token
      this.interceptFetch();
    }

    getTokenFromCookie() {
      const name = 'XSRF-TOKEN=';
      const cookies = document.cookie.split(';');

      for (let cookie of cookies) {
        while (cookie.charAt(0) === ' ') {
          cookie = cookie.substring(1);
        }
        if (cookie.indexOf(name) === 0) {
          return cookie.substring(name.length);
        }
      }
      return null;
    }

    injectTokenToForms() {
      document.querySelectorAll('form').forEach(form => {
        // 检查是否已经注入过token
        if (!form.querySelector('input[name="_csrf"]')) {
          const input = document.createElement('input');
          input.type = 'hidden';
          input.name = '_csrf';
          input.value = this.token;
          form.appendChild(input);
        }
      });
    }

    interceptFetch() {
      const originalFetch = window.fetch;

      window.fetch = function(...args) {
        const [url, options = {}] = args;

        // 为需要CSRF防护的请求自动添加token
        const csrfMethods = ['POST', 'PUT', 'DELETE', 'PATCH'];
        if (csrfMethods.includes(options.method?.toUpperCase())) {
          options.headers = {
            ...options.headers,
            'X-XSRF-Token': this.token
          };
        }

        return originalFetch.apply(this, args);
      }.bind(this);
    }
  }

  // 页面加载时自动初始化
  document.addEventListener('DOMContentLoaded', () => {
    new CsrfManager();
  });

五、实战应用:处理复杂场景

5.1 多环境配置

不同的部署环境需要不同的配置:

复制代码
// 环境配置
  const environments = {
    development: {
      secure: false,          // 开发环境HTTP也行
      sameSite: 'lax',        // 开发环境宽松一些
      maxAge: 24 * 60 * 60 * 1000,
      httpOnly: false
    },

    production: {
      secure: true,           // 生产环境必须HTTPS
      sameSite: 'strict',     // 生产环境严格同站
      maxAge: 2 * 60 * 60 * 1000, // 生产环境短一些
      httpOnly: false
    },

    testing: {
      secure: false,
      sameSite: 'strict',
      maxAge: 60 * 60 * 1000, // 测试环境最短
      httpOnly: false
    }
  };

  const env = process.env.NODE_ENV || 'development';
  const cookieConfig = environments[env];

  // 使用配置
  res.cookie('XSRF-TOKEN', token, cookieConfig);

5.2 错误处理和用户体验

当CSRF验证失败时,我们需要友好的用户体验:

复制代码
// 前端错误处理
  window.addEventListener('unhandledrejection', (event) => {
    if (event.reason?.error === 'CSRF验证失败') {
      // 显示友好的错误提示
      this.showCsrfError();

      // 尝试自动恢复
      this.attemptRecovery();
    }
  });

  showCsrfError() {
    const errorDiv = document.createElement('div');
    errorDiv.className = 'csrf-error-toast';
    errorDiv.innerHTML = `
      <div class="error-content">
        <h4>安全验证失败</h4>
        <p>页面可能已过期,正在刷新...</p>
        <button onclick="window.location.reload()">立即刷新</button>
      </div>
    `;
    errorDiv.style.cssText = `
      position: fixed;
      top: 20px;
      right: 20px;
      background: #ff4444;
      color: white;
      padding: 15px;
      border-radius: 5px;
      z-index: 9999;
    `;

    document.body.appendChild(errorDiv);
  }

  attemptRecovery() {
    // 3秒后自动刷新页面
    setTimeout(() => {
      window.location.reload();
    }, 3000);
  }

5.3 性能优化

对于高流量网站,我们需要优化性能:

复制代码
// 缓存验证结果
  const verificationCache = new Map();
  const CACHE_TTL = 5000; // 5秒缓存

  const optimizedCsrfProtection = (req, res, next) => {
    if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
      return next();
    }

    // 生成缓存键
    const cacheKey = `${req.ip}-${req.cookies['XSRF-TOKEN']}`;
    const cached = verificationCache.get(cacheKey);

    // 检查缓存
    if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
      if (cached.valid) {
        return next();
      }
    }

    // 执行验证
    const cookieToken = req.cookies['XSRF-TOKEN'];
    const headerToken = req.headers['x-xsrf-token'];
    const valid = safeCompare(cookieToken, headerToken);

    // 缓存结果
    verificationCache.set(cacheKey, {
      valid,
      timestamp: Date.now()
    });

    if (!valid) {
      return res.status(403).json({ error: 'CSRF验证失败' });
    }

    next();
  };

六、常见问题和解决方案

6.1 "我的token经常失效"

问题:用户反馈经常出现CSRF验证失败

原因和解决方案:

复制代码
  // 原因1:token过期时间太短
  // 解决:适当延长token有效期
  res.cookie('XSRF-TOKEN', token, {
    maxAge: 7 * 24 * 60 * 60 * 1000 // 7天
  });

  // 原因2:多个标签页token不同步
  // 解决:使用storage事件同步token
  window.addEventListener('storage', (e) => {
    if (e.key === 'csrf-token-refresh') {
      location.reload(); // 刷新页面获取新token
    }
  });

  // 原因3:移动端网络问题
  // 解决:添加重试机制
  async function fetchWithRetry(url, options, retries = 3) {
    try {
      return await fetch(url, options);
    } catch (error) {
      if (retries > 0 && error.status === 403) {
        // 刷新token后重试
        await refreshCsrfToken();
        return fetchWithRetry(url, options, retries - 1);
      }
      throw error;
    }
  }

6.2 "在SPA应用中如何处理?"

SPA应用的特殊考虑:

复制代码
  // Vue.js示例
  import axios from 'axios';

  // 请求拦截器:自动添加CSRF token
  axios.interceptors.request.use(config => {
    const csrfMethods = ['post', 'put', 'delete', 'patch'];

    if (csrfMethods.includes(config.method?.toLowerCase())) {
      const token = getCookie('XSRF-TOKEN');
      if (token) {
        config.headers['X-XSRF-Token'] = token;
      }
    }

    return config;
  });

  // 响应拦截器:处理CSRF错误
  axios.interceptors.response.use(
    response => response,
    error => {
      if (error.response?.status === 403) {
        const data = error.response.data;
        if (data?.error?.includes('CSRF')) {
          // CSRF错误,刷新页面
          window.location.href = '/login?reason=csrf_expired';
        }
      }
      return Promise.reject(error);
    }
  );

七、总结:双重提交Cookie的优劣势

优势:

  1. 实现简单:比同步令牌模式简单得多

  2. 性能优秀:无状态,不需要服务端存储

  3. 分布式友好:天然支持多服务器部署

  4. 用户体验好:token有效期长,减少用户中断

劣势:

  1. 安全性稍低:依赖Cookie的正确配置

  2. 不支持一次性token:无法用完即焚

  3. 子域名风险:需要正确配置域名范围

适用场景:

  • 现代SPA应用

  • 高并发系统

  • 微服务架构

  • 对性能要求较高的应用

双重提交Cookie模式通过巧妙利用浏览器的同源策略,实现了既安全又高效的CSRF防护,是现代Web应用的重要安全机制。


安全审查--跨站请求伪造--同步令牌模式

安全审查--跨站请求伪造--双重提交Cookie模式

安全审查--跨站请求伪造--Fetch Metadata防护模式

CSRF防护模式选择指南:三种方案的对比与决策


相关推荐
金士镧(厦门)新材料有限公司4 小时前
稀土化合物:科技与日常生活的“隐形助力”
科技·安全·全文检索·生活·能源
艾莉丝努力练剑4 小时前
【Linux基础开发工具 (七)】Git 版本管理全流程与 GDB / CGDB 调试技巧
大数据·linux·运维·服务器·git·安全·elasticsearch
悬镜安全4 小时前
悬镜安全率先通过国家工信安全中心SBOM标准认证
安全
cike_y4 小时前
JavaWeb之HttpServletResponse
java·开发语言·安全·java安全
虹科网络安全4 小时前
艾体宝洞察 | “顶会”看安全(三):Black hat-从底层突破AI安全 :利用 NVIDIA 漏洞实现容器逃逸
人工智能·安全
艾莉丝努力练剑4 小时前
【Python基础:语法第五课】Python字典高效使用指南:避开KeyError,掌握遍历与增删改查精髓
大数据·运维·人工智能·python·安全·pycharm
xixixi777774 小时前
App反诈骗:一场面向移动生态的深度安全战争(接上文短信反诈)
安全·信息与通信·通信·电话反诈
Guheyunyi4 小时前
古河云科技智慧消防解决方案
大数据·人工智能·科技·安全·信息可视化·架构
西***63474 小时前
分布式・低延时・高安全!新一代 KVM 坐席系统,重塑智能管控新生态
分布式·安全