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

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

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

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

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

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


摘要:从小白开始逐层讲解Fetch Metadata

一、从一个现代浏览器的困惑说起

1.1 新时代的安全挑战

想象一下这个场景:你正在开发一个现代化的电商网站,使用了最新的前端框架和API设计。有一天,产品经理找到你说:

"我们的安全团队报告说,虽然我们已经有了CSRF防护,但在移动端和一些现代浏览器上还是可能存在风险。而且我们不想增加太多复杂性,有没有更简单的方法?"

你心想:"都已经2024年了,难道浏览器本身不能提供一些保护机制吗?"

答案是:能!而且比你想象的还要强大!

1.2 浏览器的"自报家门"机制

现代浏览器实际上会给服务器发送一些"身份信息",告诉服务器这个请求是从哪里来的,想要做什么。这就是Fetch Metadata机制。

一个形象的比喻:

想象一下银行的安全系统:

传统方式:

客户进门 → 银行问:"你是谁?" → 客户回答:"我是张三" → 银行核实

问题:任何人都可以说"我是张三"

Fetch Metadata方式:

客户进门 → 银行问:"你是谁?从哪里来?要办什么业务?"

客户回答:"我是张三,从正门进来,要办理转账业务"

银行验证:张三确实是从正门进来的,办理的业务类型也合理

**这就是Fetch Metadata的核心思想:**不仅要知道"你是谁",还要知道"你是怎么来的,想干什么"。

1.3 理解请求的"元数据"

每个HTTP请求都会附带一些"元数据",描述这个请求的背景:

复制代码
  # 这些头是浏览器自动添加的,开发者无法伪造
  Sec-Fetch-Site: cross-site      # 我是从别的网站来的
  Sec-Fetch-Mode: cors            # 我是一个跨域请求
  Sec-Fetch-Dest: empty           # 我的目标不是特定资源

这相当于浏览器在向服务器"汇报":

  • "这个请求是从什么类型的网站发起的"

  • "这个请求是什么模式"

  • "这个请求想要获取什么类型的资源"

二、深入理解Fetch Metadata的工作原理

2.1 三大核心请求头详解

Sec-Fetch-Site:请求的"出身"

这个头告诉我们请求发起的网站类型:

复制代码
  // 不同来源的示例
  console.log('请求来源分析:');
  console.log('1. https://app.example.com → https://api.example.com');
  console.log('   Sec-Fetch-Site: same-site (同一站点)');
  console.log('2. https://app.example.com → https://other.com');
  console.log('   Sec-Fetch-Site: cross-site (跨站点)');
  console.log('3. 用户直接在地址栏输入URL');
  console.log('   Sec-Fetch-Site: none (直接访问)');
  console.log('4. https://sub.example.com → https://example.com');
  console.log('   Sec-Fetch-Site: same-site (子域名也算同站)');

四种可能的值:

复制代码
  - same-origin:完全同源(协议、域名、端口都相同)
  - same-site:同站点(可能是不同子域名)
  - cross-site:跨站点(完全不同的域名)
  - none:用户直接发起(地址栏输入、书签等)

Sec-Fetch-Mode:请求的"方式"

这个头描述请求是如何发起的:

复制代码
  // 不同模式的请求
  console.log('请求模式分析:');
  console.log('1. 用户点击链接:<a href="...">');
  console.log('   Sec-Fetch-Mode: navigate (导航模式)');
  console.log('2. fetch("https://api.example.com/data")');
  console.log('   Sec-Fetch-Mode: cors (跨域模式)');
  console.log('3. <img src="https://example.com/image.jpg">');
  console.log('   Sec-Fetch-Mode: no-cors (图片加载模式)');
  console.log('4. 内部API调用:fetch("/api/users")');
  console.log('   Sec-Fetch-Mode: same-origin (同源模式)');

Sec-Fetch-Dest:请求的"目标"

这个头说明请求想要获取什么类型的资源:

复制代码
  // 不同目标的请求
  console.log('请求目标分析:');
  console.log('1. fetch("/api/users")');
  console.log('   Sec-Fetch-Dest: empty (不明确的资源类型)');
  console.log('2. <iframe src="...">');
  console.log('   Sec-Fetch-Dest: iframe (iframe内容)');
  console.log('3. <script src="...">');
  console.log('   Sec-Fetch-Dest: script (JavaScript文件)');
  console.log('4. <img src="...">');
  console.log('   Sec-Fetch-Dest: image (图片资源)');

2.2 浏览器如何"自动"添加这些头

关键在于:这些头是浏览器自动添加的,JavaScript无法伪造!

复制代码
  <!-- 恶意网站的代码 -->
  <script>
  // 攻击者尝试伪造请求头
  fetch('https://bank.com/api/transfer', {
    method: 'POST',
    headers: {
      // 伪造攻击者的"梦想"
      'Sec-Fetch-Site': 'same-origin',  // 妄图伪装成同源请求
      'Sec-Fetch-Mode': 'cors',
      'Sec-Fetch-Dest': 'empty',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ to: 'hacker', amount: 1000 })
  });
  </script>

实际发生的情况:

复制代码
  POST /api/transfer HTTP/1.1
  Host: bank.com
  Content-Type: application/json

  # 浏览器忽略攻击者的伪造,添加真实信息
  Sec-Fetch-Site: cross-site        # 真相:这是跨站请求!
  Sec-Feach-Mode: cors             # 这是CORS请求
  Sec-Fetch-Dest: empty            # 目标不明确
  Cookie: session_id=abc123        # 自动携带银行Cookie

银行服务器一看:

"等等!这个请求说是从same-origin来的,但Sec-Fetch-Site显示是cross-site!

这不就是CSRF攻击吗?拒绝!"

三、从零开始实现Fetch Metadata防护

3.1 最基础的实现

让我们从最简单的防护开始:

复制代码
  // 基础的Fetch Metadata防护中间件
  const basicFetchMetadataProtection = (req, res, next) => {
    // GET请求通常安全,跳过检查
    if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
      return next();
    }

    // 获取Fetch Metadata头
    const fetchSite = req.headers['sec-fetch-site'];
    const fetchMode = req.headers['sec-fetch-mode'];
    const fetchDest = req.headers['sec-fetch-dest'];

    // 简单规则:拒绝跨站点的POST请求
    if (fetchSite === 'cross-site' && req.method !== 'GET') {
      return res.status(403).json({
        error: 'CSRF防护:跨站点请求被拒绝',
        code: 'CROSS_SITE_REQUEST_BLOCKED'
      });
    }

    next();
  };

  // 应用中间件
  app.use('/api', basicFetchMetadataProtection);

这个简单的实现已经能防护大部分CSRF攻击了!

3.2 更细致的规则制定

现在让我们制定更精确的防护规则:

复制代码
  const advancedFetchMetadataProtection = (req, res, next) => {
    if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
      return next();
    }

    const fetchSite = req.headers['sec-fetch-site'];
    const fetchMode = req.headers['sec-fetch-mode'];
    const fetchDest = req.headers['sec-fetch-dest'];

    // 规则1:检查请求来源
    if (fetchSite === 'cross-site') {
      // 跨站点请求只允许特定情况
      if (!isAllowedCrossSiteRequest(req, fetchMode, fetchDest)) {
        return res.status(403).json({
          error: '跨站点请求不被允许',
          code: 'CROSS_SITE_NOT_ALLOWED',
          details: {
            fetchSite,
            fetchMode,
            fetchDest,
            method: req.method
          }
        });
      }
    }

    // 规则2:检查请求逻辑是否合理
    if (fetchMode === 'navigate' && req.method !== 'GET') {
      // 导航请求通常是GET方法
      return res.status(403).json({
        error: '可疑的导航请求',
        code: 'SUSPICIOUS_NAVIGATE_REQUEST'
      });
    }

    // 规则3:检查请求目标是否合理
    if (fetchDest === 'script' && req.method !== 'GET') {
      // 脚本资源只能通过GET获取
      return res.status(403).json({
        error: '脚本资源请求方法错误',
        code: 'INVALID_SCRIPT_REQUEST'
      });
    }

    next();
  };

  // 辅助函数:判断是否允许跨站点请求
  function isAllowedCrossSiteRequest(req, fetchMode, fetchDest) {
    // 允许表单提交(用户主动操作)
    if (fetchMode === 'navigate' && fetchDest === 'document') {
      return true;
    }

    // 允许公开API的CORS请求
    const publicApis = ['/api/public/', '/api/webhooks/'];
    if (publicApis.some(api => req.path.startsWith(api)) && fetchMode === 'cors') {
      return true;
    }

    return false;
  }

3.3 分层防护策略

不同的API端点需要不同的防护级别:

复制代码
  // 分层防护配置
  const protectionLevels = {
    // 最低级别:公开API
    public: {
      paths: ['/api/public/', '/api/docs/', '/api/health'],
      rules: [
        'allow-cross-site-cors',
        'basic-validation'
      ]
    },

    // 中等级别:普通用户API
    user: {
      paths: ['/api/user/', '/api/profile/'],
      rules: [
        'block-cross-site-requests',
        'validate-navigation-intent'
      ]
    },

    // 最高级别:敏感管理API
    admin: {
      paths: ['/api/admin/', '/api/delete/', '/api/transfer/'],
      rules: [
        'require-same-origin',
        'strict-validation',
        'enhanced-logging'
      ]
    }
  };

  const layeredProtection = (req, res, next) => {
    // 确定请求的防护级别
    const level = determineProtectionLevel(req.path);

    // 应用对应级别的规则
    applyProtectionRules(req, res, level, () => {
      next();
    });
  };

  function determineProtectionLevel(path) {
    for (const [levelName, config] of Object.entries(protectionLevels)) {
      if (config.paths.some(p => path.startsWith(p))) {
        return levelName;
      }
    }
    return 'user'; // 默认级别
  }

四、进阶实现:生产级Fetch Metadata防护

4.1 智能规则引擎

让我们创建一个灵活的规则引擎:

复制代码
  class FetchMetadataRuleEngine {
    constructor() {
      this.rules = new Map();
      this.loadDefaultRules();
    }

    loadDefaultRules() {
      // 规则:阻止跨站点的状态修改请求
      this.rules.set('block-cross-site-state-change', {
        name: '阻止跨站点状态修改',
        description: '阻止来自跨站点的POST、PUT、DELETE请求',
        priority: 100, // 高优先级
        condition: (req, metadata) => {
          const stateChangeMethods = ['POST', 'PUT', 'DELETE', 'PATCH'];
          return metadata.fetchSite === 'cross-site' &&
                 stateChangeMethods.includes(req.method);
        },
        action: 'block',
        message: '跨站点的状态修改请求被拒绝'
      });

      // 规则:允许合法的表单提交
      this.rules.set('allow-form-submission', {
        name: '允许表单提交',
        description: '允许用户主动发起的表单提交',
        priority: 90,
        condition: (req, metadata) => {
          return metadata.fetchMode === 'navigate' &&
                 metadata.fetchDest === 'document';
        },
        action: 'allow',
        message: '合法的表单提交'
      });

      // 规则:检查API请求的合理性
      this.rules.set('validate-api-request', {
        name: '验证API请求合理性',
        description: '检查API请求的目标和方法是否匹配',
        priority: 80,
        condition: (req, metadata) => {
          const isApiRequest = req.path.startsWith('/api/');
          const isJsonRequest = req.headers['content-type']?.includes('application/json');

          return isApiRequest && isJsonRequest &&
                 metadata.fetchDest === 'empty' &&
                 metadata.fetchMode === 'cors';
        },
        action: 'allow',
        message: '合法的API请求'
      });
    }

    evaluate(req, metadata) {
      // 按优先级排序规则
      const sortedRules = Array.from(this.rules.entries())
        .sort(([, a], [, b]) => b.priority - a.priority);

      for (const [ruleName, rule] of sortedRules) {
        if (rule.condition(req, metadata)) {
          return {
            ruleName,
            matched: true,
            action: rule.action,
            message: rule.message,
            priority: rule.priority
          };
        }
      }

      return {
        matched: false,
        action: 'allow', // 默认允许
        message: '没有匹配的规则'
      };
    }

    addCustomRule(name, rule) {
      this.rules.set(name, rule);
      console.log(`新增自定义规则: ${rule.name}`);
    }
  }

  // 使用规则引擎
  const ruleEngine = new FetchMetadataRuleEngine();

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

    const metadata = extractMetadata(req);
    const result = ruleEngine.evaluate(req, metadata);

    if (result.action === 'block') {
      logSecurityEvent('REQUEST_BLOCKED', req, metadata, result);
      return res.status(403).json({
        error: result.message,
        code: 'SECURITY_VIOLATION',
        rule: result.ruleName
      });
    }

    logSecurityEvent('REQUEST_ALLOWED', req, metadata, result);
    next();
  };

  function extractMetadata(req) {
    return {
      fetchSite: req.headers['sec-fetch-site'],
      fetchMode: req.headers['sec-fetch-mode'],
      fetchDest: req.headers['sec-fetch-dest'],
      fetchUser: req.headers['sec-fetch-user'],
      // 检查是否有完整的Fetch Metadata
      isComplete: !!(req.headers['sec-fetch-site'] &&
                    req.headers['sec-fetch-mode'] &&
                    req.headers['sec-fetch-dest'])
    };
  }

4.2 安全事件监控和告警

生产环境需要完善的监控系统:

复制代码
  class SecurityMonitor {
    constructor() {
      this.events = [];
      this.alerts = [];
      this.thresholds = {
        suspiciousRate: 0.05,    // 5%的可疑请求率
        blockRate: 0.1,          // 10%的拦截率
        attackThreshold: 10      // 每分钟10次攻击
      };

      this.startMonitoring();
    }

    recordSecurityEvent(type, req, metadata, result) {
      const event = {
        type,
        timestamp: new Date().toISOString(),
        ip: req.ip,
        userAgent: req.headers['user-agent'],
        method: req.method,
        url: req.url,
        referer: req.headers.referer,
        metadata,
        result
      };

      this.events.push(event);

      // 只保留最近1000个事件
      if (this.events.length > 1000) {
        this.events.shift();
      }

      // 实时分析
      this.analyzeEvent(event);
    }

    analyzeEvent(event) {
      // 检查可疑模式
      if (this.isSuspiciousEvent(event)) {
        this.handleSuspiciousEvent(event);
      }

      // 检查攻击模式
      if (this.isAttackPattern(event)) {
        this.handleAttackEvent(event);
      }

      // 检查异常模式
      if (this.isAnomalousPattern(event)) {
        this.handleAnomalousEvent(event);
      }
    }

    isSuspiciousEvent(event) {
      return (
        !event.metadata.isComplete ||                           // 缺少Metadata
        event.metadata.fetchSite === 'cross-site' ||           // 跨站点请求
        this.hasSuspiciousUserAgent(event.userAgent) ||         // 可疑的User-Agent
        this.isHighRiskIp(event.ip)                            // 高风险IP
      );
    }

    isAttackPattern(event) {
      // 检查是否是明显的攻击模式
      const recentEvents = this.getRecentEvents(event.ip, 60000); // 最近1分钟

      return (
        recentEvents.filter(e => e.type === 'REQUEST_BLOCKED').length > 5 ||
        event.result?.ruleName === 'block-cross-site-state-change'
      );
    }

    isAnomalousPattern(event) {
      // 检查异常模式
      const recentEvents = this.getRecentEvents(event.ip, 3600000); // 最近1小时

      // 用户突然改变行为模式
      const uniqueUrls = new Set(recentEvents.map(e => e.url)).size;
      const uniqueMethods = new Set(recentEvents.map(e => e.method)).size;

      return uniqueUrls > 50 || uniqueMethods > 5;
    }

    handleSuspiciousEvent(event) {
      console.warn('[可疑事件]', {
        ip: event.ip,
        userAgent: event.userAgent,
        url: event.url,
        metadata: event.metadata
      });

      // 可以发送到SIEM系统
      this.sendToSIEM('SUSPICIOUS', event);
    }

    handleAttackEvent(event) {
      console.error('[攻击事件]', {
        ip: event.ip,
        attackType: event.result?.ruleName,
        url: event.url,
        timestamp: event.timestamp
      });

      // 立即告警
      this.sendImmediateAlert('ATTACK_DETECTED', event);

      // 可以临时封禁IP
      this.temporaryBlockIp(event.ip, 3600); // 封禁1小时
    }

    handleAnomalousEvent(event) {
      console.info('[异常模式]', {
        ip: event.ip,
        pattern: 'behavioral_change',
        timestamp: event.timestamp
      });

      this.sendToAnalytics('ANOMALY', event);
    }

    generateSecurityReport() {
      const now = new Date();
      const last24h = new Date(now.getTime() - 24 * 60 * 60 * 1000);

      const recentEvents = this.events.filter(e => new Date(e.timestamp) > last24h);

      const report = {
        period: 'last_24_hours',
        totalRequests: recentEvents.length,
        blockedRequests: recentEvents.filter(e => e.type === 'REQUEST_BLOCKED').length,
        suspiciousEvents: recentEvents.filter(e => e.type === 'SUSPICIOUS_EVENT').length,
        attackEvents: recentEvents.filter(e => e.type === 'ATTACK_EVENT').length,
        topAttackerIps: this.getTopAttackerIps(recentEvents),
        mostAttackedEndpoints: this.getMostAttackedEndpoints(recentEvents),
        securityScore: this.calculateSecurityScore(recentEvents)
      };

      return report;
    }

    calculateSecurityScore(events) {
      const total = events.length;
      const blocked = events.filter(e => e.type === 'REQUEST_BLOCKED').length;
      const suspicious = events.filter(e => e.type === 'SUSPICIOUS_EVENT').length;

      // 安全评分算法
      const blockRate = total > 0 ? blocked / total : 0;
      const suspiciousRate = total > 0 ? suspicious / total : 0;

      let score = 100;
      score -= blockRate * 500;      // 拦截率影响
      score -= suspiciousRate * 200; // 可疑率影响

      return Math.max(0, Math.round(score));
    }
  }

  // 创建监控实例
  const securityMonitor = new SecurityMonitor();

  function logSecurityEvent(type, req, metadata, result) {
    securityMonitor.recordSecurityEvent(type, req, metadata, result);
  }

4.3 自适应防护策略

根据实时威胁情报调整防护策略:

复制代码
  class AdaptiveProtection {
    constructor() {
      this.threatLevel = 'normal'; // low, normal, high, critical
      this.protectionLevels = {
        low: {
          strictness: 0.3,
          logging: 'minimal',
          blocking: 'selective'
        },
        normal: {
          strictness: 0.6,
          logging: 'standard',
          blocking: 'aggressive'
        },
        high: {
          strictness: 0.8,
          logging: 'verbose',
          blocking: 'strict'
        },
        critical: {
          strictness: 1.0,
          logging: 'exhaustive',
          blocking: 'maximum'
        }
      };

      this.startThreatAssessment();
    }

    startThreatAssessment() {
      // 每5分钟评估一次威胁等级
      setInterval(() => {
        this.assessThreatLevel();
        this.adjustProtection();
      }, 5 * 60 * 1000);
    }

    assessThreatLevel() {
      const report = securityMonitor.generateSecurityReport();

      // 威胁评估算法
      const attackRate = report.attackEvents / report.totalRequests;
      const blockRate = report.blockedRequests / report.totalRequests;

      if (attackRate > 0.1 || report.attackEvents > 100) {
        this.threatLevel = 'critical';
      } else if (attackRate > 0.05 || report.attackEvents > 50) {
        this.threatLevel = 'high';
      } else if (attackRate > 0.01 || blockRate > 0.1) {
        this.threatLevel = 'normal';
      } else {
        this.threatLevel = 'low';
      }

      console.log(`威胁等级调整为: ${this.threatLevel}`, report);
    }

    adjustProtection() {
      const config = this.protectionLevels[this.threatLevel];

      // 动态调整规则引擎的严格程度
      ruleEngine.rules.forEach((rule, name) => {
        if (config.strictness > 0.7) {
          // 高严格性:启用更严格的条件
          this.enhanceRuleStrictness(rule);
        } else if (config.strictness < 0.5) {
          // 低严格性:放宽一些条件
          this.relaxRuleStrictness(rule);
        }
      });

      // 发送通知
      this.notifyThreatLevelChange(this.threatLevel, config);
    }

    getMiddleware() {
      return (req, res, next) => {
        // 根据当前威胁等级调整行为
        const config = this.protectionLevels[this.threatLevel];

        if (this.threatLevel === 'critical') {
          // 关键威胁等级:额外验证
          this.applyCriticalProtection(req, res, next);
        } else {
          // 正常流程
          intelligentProtection(req, res, next);
        }
      };
    }

    applyCriticalProtection(req, res, next) {
      // 关键威胁时的额外保护措施
      console.warn(`[关键威胁] 对请求 ${req.method} ${req.path} 应用严格验证`);

      // 可以添加额外的验证步骤
      this.performAdditionalValidation(req)
        .then(() => next())
        .catch(error => {
          res.status(403).json({
            error: '威胁等级过高,请求被拒绝',
            code: 'CRITICAL_THREAT_PROTECTION'
          });
        });
    }
  }

  const adaptiveProtection = new AdaptiveProtection();

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

5.1 浏览器兼容性处理

不是所有浏览器都支持Fetch Metadata:

复制代码
  class BrowserCompatibilityManager {
    static async detectSupport() {
      return new Promise((resolve) => {
        // 创建测试请求检测支持
        const testRequest = new XMLHttpRequest();
        testRequest.open('HEAD', '/api/test-metadata', true);

        testRequest.onreadystatechange = function() {
          if (this.readyState === 2) { // HEADERS_RECEIVED
            const hasMetadata = !!(
              this.getResponseHeader('Sec-Fetch-Site') ||
              this.getResponseHeader('Sec-Fetch-Mode') ||
              this.getResponseHeader('Sec-Fetch-Dest')
            );
            resolve({
              supported: hasMetadata,
              browser: this.detectBrowser(),
              version: this.getBrowserVersion()
            });
          }
        };

        testRequest.onerror = () => resolve({ supported: false });
        testRequest.send();
      });
    }

    static createCompatibleProtection() {
      return async (req, res, next) => {
        // 检查浏览器支持
        const support = await this.detectSupport();

        if (support.supported) {
          // 现代浏览器:使用Fetch Metadata
          return intelligentProtection(req, res, next);
        } else {
          // 旧浏览器:使用其他CSRF防护
          console.warn(`浏览器不支持Fetch Metadata,使用降级方案: ${support.browser}`);
          return fallbackProtection(req, res, next);
        }
      };
    }

    static detectBrowser() {
      const ua = navigator.userAgent;

      if (ua.includes('Chrome') && !ua.includes('Edg')) return 'chrome';
      if (ua.includes('Firefox')) return 'firefox';
      if (ua.includes('Safari') && !ua.includes('Chrome')) return 'safari';
      if (ua.includes('Edg')) return 'edge';
      if (ua.includes('Opera') || ua.includes('OPR')) return 'opera';

      return 'unknown';
    }

    static getBrowserVersion() {
      const ua = navigator.userAgent;
      const match = ua.match(/(Chrome|Firefox|Safari|Edg|Opera)\/(\d+)/);
      return match ? match[2] : 'unknown';
    }
  }

  // 使用兼容性管理器
  app.use('/api', await BrowserCompatibilityManager.createCompatibleProtection());

5.2 多环境配置

复制代码
  const environments = {
    development: {
      protection: {
        mode: 'basic',
        logging: 'debug',
        strictness: 0.3
      },
      monitoring: {
        enabled: true,
        alertThreshold: 100
      }
    },

    staging: {
      protection: {
        mode: 'advanced',
        logging: 'verbose',
        strictness: 0.7
      },
      monitoring: {
        enabled: true,
        alertThreshold: 20
      }
    },

    production: {
      protection: {
        mode: 'adaptive',
        logging: 'security',
        strictness: 0.8
      },
      monitoring: {
        enabled: true,
        alertThreshold: 10
      }
    }
  };

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

  // 根据环境应用不同的配置
  function createEnvironmentSpecificProtection() {
    switch (config.protection.mode) {
      case 'basic':
        return basicFetchMetadataProtection;
      case 'advanced':
        return intelligentProtection;
      case 'adaptive':
        return adaptiveProtection.getMiddleware();
      default:
        return basicFetchMetadataProtection;
    }
  }

  app.use('/api', createEnvironmentSpecificProtection());

六、常见问题和解决方案

6.1 "为什么有些请求被误拦截了?"

问题:用户反馈正常操作被拦截

原因分析和解决方案:

复制代码
  // 原因1:用户使用旧浏览器
  // 解决:提供降级方案
  const fallbackProtection = (req, res, next) => {
    // 对于不支持Fetch Metadata的浏览器,使用传统CSRF防护
    if (!req.headers['sec-fetch-site']) {
      return traditionalCsrfProtection(req, res, next);
    }
    next();
  };

  // 原因2:某些合法的跨域请求被拦截
  // 解决:精确配置例外规则
  const whitelistRules = {
    allowedCrossSiteOrigins: [
      'https://partner.example.com',
      'https://payment.gateway.com'
    ],
    allowedCrossSitePaths: [
      '/api/webhooks/',
      '/api/callbacks/',
      '/api/public/'
    ]
  };

  const enhancedProtection = (req, res, next) => {
    const origin = req.headers.origin;
    const path = req.path;

    // 检查是否在白名单中
    if (whitelistRules.allowedCrossSiteOrigins.includes(origin) ||
        whitelistRules.allowedCrossSitePaths.some(p => path.startsWith(p))) {
      return next();
    }

    // 继续正常验证流程
    intelligentProtection(req, res, next);
  };

6.2 "如何平衡安全性和用户体验?"

策略:动态调整防护严格程度

复制代码
  class UserExperienceOptimizedProtection {
    constructor() {
      this.userTrustScores = new Map(); // 用户信任评分
    }

    middleware() {
      return (req, res, next) => {
        const userId = req.user?.id;
        const trustScore = this.getUserTrustScore(userId);

        // 高信任度用户:宽松的验证
        if (trustScore > 0.8) {
          return this.applyLenientProtection(req, res, next);
        }

        // 低信任度用户:严格验证
        if (trustScore < 0.3) {
          return this.applyStrictProtection(req, res, next);
        }

        // 普通用户:标准验证
        intelligentProtection(req, res, next);
      };
    }

    getUserTrustScore(userId) {
      if (!userId) return 0.5; // 未登录用户的默认信任度

      const score = this.userTrustScores.get(userId) || 0.5;
      return score;
    }

    updateUserTrustScore(userId, action) {
      let score = this.getUserTrustScore(userId);

      switch (action) {
        case 'successful_login':
          score += 0.1;
          break;
        case 'security_violation':
          score -= 0.2;
          break;
        case 'long_term_user':
          score += 0.05;
          break;
      }

      score = Math.max(0, Math.min(1, score));
      this.userTrustScores.set(userId, score);
    }
  }

6.3 "Fetch Metadata能完全替代其他CSRF防护吗?"

答案:不能完全替代,建议作为深度防御的一部分

复制代码
  // 推荐的混合防护策略
  class DefenseInDepthProtection {
    constructor() {
      this.layers = [
        { name: 'fetch-metadata', enabled: true, priority: 1 },
        { name: 'rate-limiting', enabled: true, priority: 2 },
        { name: 'same-site-cookies', enabled: true, priority: 3 },
        { name: 'csrf-tokens', enabled: false, priority: 4 } // 仅在高风险时启用
      ];
    }

    middleware() {
      return async (req, res, next) => {
        let blocked = false;
        let blockReason = '';

        // 逐层检查
        for (const layer of this.layers.filter(l => l.enabled)) {
          const result = await this.executeLayer(layer.name, req);

          if (result.blocked) {
            blocked = true;
            blockReason = result.reason;
            break;
          }
        }

        if (blocked) {
          return res.status(403).json({
            error: '多层安全防护拒绝请求',
            reason: blockReason,
            layers: this.layers.map(l => l.name)
          });
        }

        next();
      };
    }

    async executeLayer(layerName, req) {
      switch (layerName) {
        case 'fetch-metadata':
          return this.checkFetchMetadata(req);
        case 'rate-limiting':
          return this.checkRateLimiting(req);
        case 'same-site-cookies':
          return this.checkSameSite(req);
        case 'csrf-tokens':
          return this.checkCsrfToken(req);
        default:
          return { blocked: false };
      }
    }
  }

七、总结:Fetch Metadata的优势和局限

优势:

  1. 零实现成本:浏览器原生支持,无需复杂的token管理

  2. 高性能:仅需HTTP头解析,无额外计算开销

  3. 无状态:天然支持分布式和微服务架构

  4. 现代安全:基于浏览器的最新安全特性

局限性:

  1. 浏览器依赖:需要现代浏览器支持

  2. 信息有限:只能判断请求的"出身",无法完全确定用户意图

  3. 配置复杂:需要精确的规则制定和调整

  4. 不能单独使用:建议作为深度防御的一部分

适用场景:

  • 现代Web应用(用户使用较新浏览器)

  • 高并发API服务

  • 微服务架构

  • 对性能要求极高的场景

  • 需要快速部署安全防护的项目

Fetch Metadata代表了Web安全的未来方向,它让我们能够利用浏览器本身的安全能力,构建更智能、更高效的防护系统。虽然不能完全替代传统的CSRF防护,但作为现代Web安全架构的重要组成部分,它正在发挥着越来越重要的作用。


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

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

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

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


相关推荐
BullSmall4 小时前
第三方软件安全-Dependency‑Track Docker 部署
安全·docker·容器
AImatters4 小时前
国产算力底座,支撑油气储运数字化迈过关键分水岭
安全·cpu·算力·海光·油气储运
叠叠乐7 小时前
中国移动家庭云电脑window关闭所有安全脚本
安全
Jmyd01237 小时前
实训室的 3D 模型涉及肖像文物,数据安全与合规怎么做?
安全·3d·数据安全·虚拟实训
芯盾时代8 小时前
《金融业网络安全管理办法(征求意见稿)》全条款深度拆解(三)
网络·安全·网络安全
敢敢是只喵i8 小时前
一个本地 AI Agent 要操作多个门店或 SaaS 账号,应该怎样安全切换身份?
人工智能·安全·ai·系统架构·业界资讯
Acrel12349 小时前
DC800V 大规模落地,算力机房安全该如何保障
安全
其实防守也摸鱼10 小时前
权限提升与横向移动:从内网渗透到域控的完整技术图谱
运维·服务器·数据库·安全·github·copilot·渗透
深圳海导科技10 小时前
资产防盗追踪系统:物联网高价值资产安全管理|海导科技navynav
科技·物联网·安全
波多利希最高礼仪的握力器10 小时前
安全复制SCP
linux·服务器·安全