每天一个高级前端知识 - Day 17
今日主题:前端安全 - XSS、CSRF、点击劫持等攻击的防御实战
核心概念:安全不是后补的补丁,而是设计的基因
前端安全是纵深防御体系,需要在代码层面、传输层面、运维层面都建立防护。
🔐 安全威胁全景图
┌─────────────────────────────────────────────┐
│ 客户端攻击 │
├─────────────────────────────────────────────┤
│ • XSS (跨站脚本攻击) ★★★★★ │
│ • CSRF (跨站请求伪造) ★★★★☆ │
│ • Clickjacking (点击劫持) ★★★☆☆ │
│ • 第三方库漏洞 ★★★★☆ │
│ • 敏感信息泄露 ★★★★★ │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 传输层攻击 │
├─────────────────────────────────────────────┤
│ • MITM (中间人攻击) ★★★★★ │
│ • DNS劫持 ★★★★☆ │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 服务端攻击 │
├─────────────────────────────────────────────┤
│ • SQL注入 ★★★★★ │
│ • 越权访问 ★★★★☆ │
└─────────────────────────────────────────────┘
🛡️ XSS防御(跨站脚本攻击)
// ============ XSS防御工具库 ============
class XSSDefender {
// 1. HTML转义(输出编码)
static escapeHtml(str) {
if (!str) return '';
const htmlEscapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
'`': '`',
'=': '='
};
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();
🎯 今日挑战
实现一个完整的安全加固框架,要求:
- XSS防御(HTML/JS/URL/CSS转义 + CSP)
- CSRF防御(Token + SameSite + Referer验证)
- Clickjacking防御(X-Frame-Options + frame busting)
- 敏感信息加密存储
- 安全事件监控与上报
- 依赖漏洞扫描(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插件开发
💡 安全铁律:"永远不要相信用户输入"——即使是数据库里取出的数据,也要在输出时进行转义!