每天一个高级前端知识 - Day 17

6 阅读4分钟

每天一个高级前端知识 - Day 17

今日主题:前端安全 - XSS、CSRF、点击劫持等攻击的防御实战

核心概念:安全不是后补的补丁,而是设计的基因

前端安全是纵深防御体系,需要在代码层面、传输层面、运维层面都建立防护。

🔐 安全威胁全景图

┌─────────────────────────────────────────────┐
│              客户端攻击                       │
├─────────────────────────────────────────────┤
│ • XSS (跨站脚本攻击)          ★★★★★         │
│ • CSRF (跨站请求伪造)         ★★★★☆         │
│ • Clickjacking (点击劫持)     ★★★☆☆         │
│ • 第三方库漏洞                ★★★★☆         │
│ • 敏感信息泄露                ★★★★★         │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│              传输层攻击                       │
├─────────────────────────────────────────────┤
│ • MITM (中间人攻击)            ★★★★★        │
│ • DNS劫持                     ★★★★☆         │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│              服务端攻击                       │
├─────────────────────────────────────────────┤
│ • SQL注入                     ★★★★★         │
│ • 越权访问                    ★★★★☆         │
└─────────────────────────────────────────────┘

🛡️ XSS防御(跨站脚本攻击)

// ============ XSS防御工具库 ============
class XSSDefender {
  
  // 1. HTML转义(输出编码)
  static escapeHtml(str) {
    if (!str) return '';
    
    const htmlEscapeMap = {
      '&': '&',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#x27;',
      '/': '&#x2F;',
      '`': '&#x60;',
      '=': '&#x3D;'
    };
    
    return String(str).replace(/[&<>"'`=/]/g, (char) => htmlEscapeMap[char]);
  }
  
  // 2. JS转义(用于JS字符串上下文)
  static escapeJs(str) {
    if (!str) return '';
    
    return String(str).replace(/[\\'"\n\r\u2028\u2029]/g, (char) => {
      switch (char) {
        case '\\': return '\\\\';
        case "'": return "\\'";
        case '"': return '\\"';
        case '\n': return '\\n';
        case '\r': return '\\r';
        case '\u2028': return '\\u2028';
        case '\u2029': return '\\u2029';
        default: return char;
      }
    });
  }
  
  // 3. URL转义
  static escapeUrl(str) {
    if (!str) return '';
    return encodeURIComponent(str);
  }
  
  // 4. CSS转义
  static escapeCss(str) {
    if (!str) return '';
    return String(str).replace(/[\\"']/g, (char) => `\\${char}`);
  }
  
  // 5. 安全的innerHTML替代
  static setSafeHTML(element, html) {
    // 使用DOMPurify清洗HTML
    if (window.DOMPurify) {
      element.innerHTML = DOMPurify.sanitize(html, {
        ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
        ALLOWED_ATTR: ['href', 'target'],
        ALLOW_DATA_ATTR: false
      });
    } else {
      element.textContent = this.escapeHtml(html);
    }
  }
  
  // 6. 安全的eval替代(绝不使用eval)
  static safeFunction(code, context = {}) {
    // 使用Function构造函数,限制作用域
    const fn = new Function('context', `with(context) { return ${code} }`);
    return fn(context);
  }
  
  // 7. 检测XSS payload
  static detectXSS(input) {
    const xssPatterns = [
      /<script.*?>.*?<\/script>/gi,
      /javascript:/gi,
      /onerror\s*=/gi,
      /onload\s*=/gi,
      /onclick\s*=/gi,
      /eval\s*\(/gi,
      /expression\s*\(/gi
    ];
    
    for (const pattern of xssPatterns) {
      if (pattern.test(input)) {
        return true;
      }
    }
    return false;
  }
}

// ============ 内容安全策略(CSP) ============
// 通过HTTP响应头设置
// Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval';

// 或使用meta标签
// <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://trusted-cdn.com">

// 动态生成nonce
const generateNonce = () => {
  return Math.random().toString(36).substring(2, 15);
};

const cspNonce = generateNonce();

// 添加到script标签
const script = document.createElement('script');
script.nonce = cspNonce;
script.src = 'app.js';
document.head.appendChild(script);

// ============ React中的XSS防御 ============
const SafeComponent = ({ userInput }) => {
  // React默认会转义,但dangerouslySetInnerHTML很危险
  // ❌ 危险
  const dangerous = { __html: userInput };
  
  // ✅ 安全 - 使用DOMPurify
  const safeHtml = DOMPurify.sanitize(userInput);
  
  return (
    <div>
      {/* 自动转义 */}
      <p>{userInput}</p>
      
      {/* 清洗后使用 */}
      <div dangerouslySetInnerHTML={{ __html: safeHtml }} />
    </div>
  );
};

🎭 CSRF防御(跨站请求伪造)

// ============ CSRF防御工具类 ============
class CSRFDefender {
  constructor() {
    this.tokenName = 'X-CSRF-Token';
    this.token = null;
    this.init();
  }
  
  // 1. 生成并获取CSRF Token
  async init() {
    // 从Cookie或meta获取token
    this.token = this.getCookie('csrf_token') || 
                 document.querySelector('meta[name="csrf-token"]')?.content;
    
    if (!this.token) {
      await this.fetchToken();
    }
  }
  
  async fetchToken() {
    const response = await fetch('/api/csrf-token', {
      credentials: 'include'
    });
    const data = await response.json();
    this.token = data.token;
    this.setCookie('csrf_token', this.token);
  }
  
  // 2. 添加Token到请求
  addTokenToRequest(config) {
    if (!this.token) return config;
    
    config.headers = {
      ...config.headers,
      [this.tokenName]: this.token
    };
    
    return config;
  }
  
  // 3. 包装fetch
  fetchWithCSRF(url, options = {}) {
    const config = this.addTokenToRequest(options);
    return fetch(url, config);
  }
  
  // 4. SameSite Cookie(最有效)
  // Set-Cookie: sessionId=abc123; SameSite=Strict; Secure
  
  // 5. 双重验证(检查Referer)
  static validateReferer(request) {
    const referer = request.headers.get('Referer');
    const origin = request.headers.get('Origin');
    const allowedDomain = 'https://yourdomain.com';
    
    if (!referer && !origin) return false;
    if (referer && !referer.startsWith(allowedDomain)) return false;
    if (origin && !origin.startsWith(allowedDomain)) return false;
    
    return true;
  }
  
  getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop()?.split(';').shift();
  }
  
  setCookie(name, value, days = 1) {
    const date = new Date();
    date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
    document.cookie = `${name}=${value}; expires=${date.toUTCString()}; path=/; SameSite=Strict; Secure`;
  }
}

// ============ 使用示例 ============
const csrfDefender = new CSRFDefender();

// 自动化的fetch拦截
const originalFetch = window.fetch;
window.fetch = function(url, options) {
  return csrfDefender.fetchWithCSRF(url, options);
};

// 或者在请求拦截器中添加
axios.interceptors.request.use(config => {
  config.headers['X-CSRF-Token'] = csrfDefender.token;
  return config;
});

🖱️ Clickjacking防御(点击劫持)

<!-- ============ 1. X-Frame-Options响应头 ============ -->
<!-- 拒绝所有iframe嵌入 -->
<!-- X-Frame-Options: DENY -->

<!-- 只允许同域嵌入 -->
<!-- X-Frame-Options: SAMEORIGIN -->

<!-- ============ 2. CSP frame-ancestors ============ -->
<!-- 更精细的控制 -->
<!-- Content-Security-Policy: frame-ancestors 'self' https://trusted.com -->

<!-- ============ 3. 前端防御代码(反frame busting) ============ -->
<script>
// 检测是否在iframe中
if (window.top !== window.self) {
  // 被嵌入,跳出iframe
  window.top.location = window.self.location;
}

// 或显示警告
if (window.top !== window.self) {
  document.body.innerHTML = '<h1>检测到非法嵌入,已阻止</h1>';
  throw new Error('Clickjacking detected');
}
</script>

<!-- ============ 4. 现代防御方案 ============ -->
<style>
  /* 使用CSS检测 */
  body {
    display: none;
  }
</style>
<script>
if (window.top === window.self) {
  document.body.style.display = 'block';
} else {
  // 报告点击劫持尝试
  fetch('/api/security/clickjacking-attempt', {
    method: 'POST',
    body: JSON.stringify({ referrer: document.referrer, timestamp: Date.now() })
  });
}
</script>

🔒 敏感信息保护

// ============ 敏感信息保护工具 ============
class SecureStorage {
  constructor() {
    this.secretKey = null;
    this.initEncryption();
  }
  
  // 1. 使用SubtleCrypto加密存储
  async initEncryption() {
    // 生成或获取密钥(存储在内存中)
    const storedKey = sessionStorage.getItem('encryption_key');
    
    if (storedKey) {
      const keyData = Uint8Array.from(atob(storedKey), c => c.charCodeAt(0));
      this.secretKey = await crypto.subtle.importKey(
        'raw',
        keyData,
        'AES-GCM',
        true,
        ['encrypt', 'decrypt']
      );
    } else {
      this.secretKey = await crypto.subtle.generateKey(
        { name: 'AES-GCM', length: 256 },
        true,
        ['encrypt', 'decrypt']
      );
      
      const exported = await crypto.subtle.exportKey('raw', this.secretKey);
      sessionStorage.setItem('encryption_key', btoa(String.fromCharCode(...new Uint8Array(exported))));
    }
  }
  
  // 加密存储
  async setItem(key, value) {
    const encoded = new TextEncoder().encode(JSON.stringify(value));
    const iv = crypto.getRandomValues(new Uint8Array(12));
    
    const encrypted = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      this.secretKey,
      encoded
    );
    
    const storageData = {
      iv: Array.from(iv),
      data: Array.from(new Uint8Array(encrypted))
    };
    
    localStorage.setItem(key, JSON.stringify(storageData));
  }
  
  // 解密读取
  async getItem(key) {
    const storageData = localStorage.getItem(key);
    if (!storageData) return null;
    
    const { iv, data } = JSON.parse(storageData);
    const decrypted = await crypto.subtle.decrypt(
      { name: 'AES-GCM', iv: new Uint8Array(iv) },
      this.secretKey,
      new Uint8Array(data)
    );
    
    return JSON.parse(new TextDecoder().decode(decrypted));
  }
  
  // 2. 不要将敏感信息存储在localStorage/sessionStorage中
  // 敏感信息应该存储在内存中
  
  // 3. 使用HTTP-only Cookie存储认证Token
  // Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict
  
  // 4. 避免在console中泄露信息
  static preventConsoleLeak() {
    if (process.env.NODE_ENV === 'production') {
      // 禁用console
      const noop = () => {};
      window.console.log = noop;
      window.console.info = noop;
      window.console.debug = noop;
      window.console.warn = noop;
      
      // 但保留error用于调试
      // window.console.error = noop;
    }
  }
}

// 5. 隐藏源代码映射
// 生产环境不生成sourcemap或不上传
// webpack配置: devtool: false

// 6. 避免URL中泄露敏感信息
class SafeURL {
  static encodeParams(params, sensitiveKeys = ['token', 'password', 'secret']) {
    const safeParams = { ...params };
    
    for (const key of sensitiveKeys) {
      if (safeParams[key]) {
        safeParams[key] = '***REDACTED***';
      }
    }
    
    return safeParams;
  }
  
  static removeSensitiveFromURL(url) {
    const urlObj = new URL(url);
    for (const [key, value] of urlObj.searchParams) {
      if (value.includes('token') || key.includes('secret')) {
        urlObj.searchParams.set(key, '***');
      }
    }
    return urlObj.toString();
  }
}

📦 第三方库安全

// ============ 依赖安全检查 ============
// package.json中添加检查脚本
{
  "scripts": {
    "audit": "npm audit --audit-level=high",
    "safety-check": "npx snyk test"
  },
  "devDependencies": {
    "snyk": "^1.0.0"
  }
}

// ============ 子资源完整性(SRI) ============
// 为CDN资源添加完整性哈希
<script 
  src="https://cdn.example.com/library.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous">
</script>

// 生成SRI哈希
const crypto = require('crypto');
const fs = require('fs');

function generateSRI(filePath) {
  const content = fs.readFileSync(filePath);
  const hash = crypto.createHash('sha384').update(content).digest('base64');
  return `sha384-${hash}`;
}

// ============ 依赖更新策略 ============
class DependencySecurity {
  // 1. 定期更新依赖
  static async checkOutdated() {
    const { exec } = require('child_process');
    return new Promise((resolve) => {
      exec('npm outdated --json', (error, stdout) => {
        resolve(JSON.parse(stdout));
      });
    });
  }
  
  // 2. 检测已知漏洞
  static async audit() {
    const exec = require('child_process').exec;
    return new Promise((resolve) => {
      exec('npm audit --json', (error, stdout) => {
        resolve(JSON.parse(stdout));
      });
    });
  }
  
  // 3. 使用依赖锁文件
  // 提交package-lock.json或yarn.lock到版本控制
  
  // 4. 使用自动化机器人(Dependabot)
}

🚨 安全监控与事件响应

// ============ 安全监控系统 ============
class SecurityMonitor {
  constructor() {
    this.violations = [];
    this.setupObservers();
  }
  
  setupObservers() {
    // 1. 监控CSP违规
    document.addEventListener('securitypolicyviolation', (e) => {
      this.reportViolation({
        type: 'CSP',
        violatedDirective: e.violatedDirective,
        blockedURI: e.blockedURI,
        sourceFile: e.sourceFile,
        lineNumber: e.lineNumber
      });
    });
    
    // 2. 监控控制台错误(可能表示XSS尝试)
    window.addEventListener('error', (e) => {
      if (e.message.includes('script') || e.message.includes('eval')) {
        this.reportViolation({
          type: 'PotentialXSS',
          message: e.message,
          filename: e.filename,
          lineno: e.lineno
        });
      }
    });
    
    // 3. 监控DOM突变(检测注入的脚本)
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        mutation.addedNodes.forEach((node) => {
          if (node.nodeType === 1) { // 元素节点
            if (node.tagName === 'SCRIPT' && !node.nonce) {
              this.reportViolation({
                type: 'UnscheduledScript',
                src: node.src,
                innerHTML: node.innerHTML.substring(0, 100)
              });
              
              // 移除可疑脚本
              node.remove();
            }
          }
        });
      });
    });
    
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  }
  
  reportViolation(violation) {
    this.violations.push({
      ...violation,
      timestamp: Date.now(),
      url: window.location.href,
      userAgent: navigator.userAgent
    });
    
    // 立即上报
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/security/violations', JSON.stringify(violation));
    }
  }
  
  // 4. 用户行为审计
  auditUserAction(action, data) {
    const auditLog = {
      action,
      data: this.sanitizeData(data),
      timestamp: Date.now(),
      userId: this.getUserId(),
      sessionId: this.getSessionId(),
      ip: this.getClientIP()
    };
    
    // 存储审计日志
    this.storeAuditLog(auditLog);
  }
  
  sanitizeData(data) {
    // 移除敏感字段
    const sensitive = ['password', 'token', 'secret', 'creditCard'];
    const sanitized = { ...data };
    
    for (const field of sensitive) {
      if (sanitized[field]) {
        sanitized[field] = '***REDACTED***';
      }
    }
    
    return sanitized;
  }
  
  getUserId() {
    return localStorage.getItem('userId') || 'anonymous';
  }
  
  getSessionId() {
    let sessionId = sessionStorage.getItem('sessionId');
    if (!sessionId) {
      sessionId = crypto.randomUUID();
      sessionStorage.setItem('sessionId', sessionId);
    }
    return sessionId;
  }
  
  getClientIP() {
    // 从服务器获取
    return fetch('/api/ip').then(r => r.json()).then(data => data.ip);
  }
  
  storeAuditLog(log) {
    // 存储到IndexedDB或发送到服务器
    const logs = JSON.parse(localStorage.getItem('audit_logs') || '[]');
    logs.push(log);
    
    if (logs.length > 100) {
      logs.shift();
    }
    
    localStorage.setItem('audit_logs', JSON.stringify(logs));
    
    // 定期批量上报
    if (logs.length % 10 === 0) {
      fetch('/api/audit/batch', {
        method: 'POST',
        body: JSON.stringify(logs),
        keepalive: true
      });
    }
  }
}

// 初始化安全监控
const securityMonitor = new SecurityMonitor();

🎯 今日挑战

实现一个完整的安全加固框架,要求:

  1. XSS防御(HTML/JS/URL/CSS转义 + CSP)
  2. CSRF防御(Token + SameSite + Referer验证)
  3. Clickjacking防御(X-Frame-Options + frame busting)
  4. 敏感信息加密存储
  5. 安全事件监控与上报
  6. 依赖漏洞扫描(CI/CD集成)
// 使用示例
const security = new SecurityFramework({
  csp: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-inline'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "https:"],
    connectSrc: ["'self'", "https://api.example.com"],
    frameAncestors: ["'none'"]
  },
  csrf: {
    enabled: true,
    tokenHeader: 'X-CSRF-Token',
    cookieName: '_csrf'
  },
  encryption: {
    enabled: true,
    algorithm: 'AES-GCM'
  }
});

await security.init();
security.protect();

明日预告:前端工程化进阶 - 自定义构建工具与Babel插件开发

💡 安全铁律:"永远不要相信用户输入"——即使是数据库里取出的数据,也要在输出时进行转义!