前端安全的七宗罪XSS、CSRF、点击劫持到供应链攻击的防御清单一、XSS 注入前端安全的万恶之源跨站脚本攻击XSS在 2026 年 OWASP Top 10 中仍然位列第三原因是前端框架的普及让开发者产生了框架自动防御 XSS的错觉。React 和 Vue 确实默认转义了 JSX/模板中的变量但当开发者使用dangerouslySetInnerHTML、v-html或直接在 DOM API 中插入未净化的内容时XSS 漏洞仍然存在。三类 XSS 的防御策略存储型 XSS服务端存储恶意脚本其他用户访问时触发。防御输入验证 输出编码。反射型 XSS恶意脚本嵌入 URL 参数即时反射执行。防御对所有 URL 参数进行编码处理。DOM 型 XSS客户端 JavaScript 直接操作 DOM 时注入。防御避免使用innerHTML使用textContent替代。// xss-defense.ts — XSS 防御工具集 import DOMPurify from dompurify; // 安全 HTML 渲染 interface SanitizeConfig { /** 允许的 HTML 标签白名单 */ allowedTags?: string[]; /** 允许的属性白名单 */ allowedAttributes?: Recordstring, string[]; /** 最大输入长度防止 DoS */ maxLength?: number; } const DEFAULT_SANITIZE_CONFIG: RequiredSanitizeConfig { allowedTags: [b, i, em, strong, a, p, br, ul, ol, li], allowedAttributes: { a: [href, target, rel], }, maxLength: 50000, }; /** * 净化用户输入的 HTML 内容防止 XSS 攻击 */ function sanitizeHtml( dirty: string, config: SanitizeConfig {}, ): { clean: string; wasSanitized: boolean } { const cfg { ...DEFAULT_SANITIZE_CONFIG, ...config }; // 输入长度检查防止 DoS 攻击 if (dirty.length cfg.maxLength) { throw new Error(输入内容过长${dirty.length} 字符已拒绝处理); } const clean DOMPurify.sanitize(dirty, { ALLOWED_TAGS: cfg.allowedTags, ALLOWED_ATTR: cfg.allowedAttributes ? Object.keys(cfg.allowedAttributes) : undefined, }); return { clean, wasSanitized: clean ! dirty, }; } // URL 安全校验 /** * 校验并净化 URL防止 javascript: 和 data: 协议注入 */ function sanitizeUrl(url: string): string | null { // 去除前后空白 const trimmed url.trim(); if (!trimmed) return null; try { const parsed new URL(trimmed); // 只允许 http/https 协议 if (![http:, https:].includes(parsed.protocol)) { console.warn(拒绝非安全协议的 URL: ${parsed.protocol}); return null; } return parsed.href; } catch { // 相对路径验证不包含危险协议 if (/^(javascript|data|vbscript):/i.test(trimmed)) { console.warn(拒绝危险协议的 URL); return null; } return trimmed; } } // React 安全渲染组件 function SafeHtml({ html, className }: { html: string; className?: string }) { const [sanitizedHtml, setSanitizedHtml] useState(); useEffect(() { try { const { clean } sanitizeHtml(html); setSanitizedHtml(clean); } catch (err) { console.error(HTML 净化失败:, err instanceof Error ? err.message : String(err)); setSanitizedHtml(); } }, [html]); if (!sanitizedHtml) { return null; } return ( div className{className} dangerouslySetInnerHTML{{ __html: sanitizedHtml }} / ); } export { sanitizeHtml, sanitizeUrl, SafeHtml };二、CSRF 防御体系的演化CSRF跨站请求伪造在 SPA 时代并未消失只是攻击面从传统表单提交转移到了 API 调用。2026 年主流的防御策略已从 Synchronizer Token Pattern 演进为 SameSite Cookie 自定义请求头的双重防御。// csrf-defense.ts — 前端 CSRF 防御实践 interface CsrfConfig { /** Token 获取端点 */ tokenEndpoint: string; /** 自定义请求头名称 */ headerName: string; /** Token 过期时间毫秒 */ tokenTtl: number; } const DEFAULT_CSRF_CONFIG: CsrfConfig { tokenEndpoint: /api/csrf-token, headerName: X-CSRF-Token, tokenTtl: 60 * 60 * 1000, // 1 小时 }; class CsrfProtection { private token: string | null null; private tokenExpiry: number 0; private config: CsrfConfig; constructor(config: PartialCsrfConfig {}) { this.config { ...DEFAULT_CSRF_CONFIG, ...config }; } /** 获取或刷新 CSRF Token */ async getToken(): Promisestring { const now Date.now(); // 如果 token 仍然有效直接返回 if (this.token now this.tokenExpiry) { return this.token; } try { const response await fetch(this.config.tokenEndpoint, { method: GET, credentials: same-origin, }); if (!response.ok) { throw new Error(获取 CSRF Token 失败: HTTP ${response.status}); } const data (await response.json()) as { token: string }; this.token data.token; this.tokenExpiry now this.config.tokenTtl; return data.token; } catch (err) { console.error(CSRF Token 获取异常:, err instanceof Error ? err.message : String(err)); throw err; } } /** 为请求附加 CSRF 保护 */ async attachToRequest(init: RequestInit {}): PromiseRequestInit { const token await this.getToken(); return { ...init, headers: { ...init.headers, [this.config.headerName]: token, // 额外的 CSRF 防御层自定义请求头 X-Requested-With: XMLHttpRequest, }, credentials: same-origin, }; } } // 全局 fetch 包装器 const csrf new CsrfProtection(); async function secureFetch( url: string, init: RequestInit {}, ): PromiseResponse { // 对于修改状态的方法附加 CSRF Token const methods [POST, PUT, PATCH, DELETE]; const requiresCsrf methods.includes(init.method?.toUpperCase() ?? GET); if (requiresCsrf) { init await csrf.attachToRequest(init); } return fetch(url, init); } export { CsrfProtection, secureFetch };三、点击劫持消失的威胁还是隐藏的漏洞点击劫持Clickjacking通过透明 iframe 诱使用户点击看似无害的页面元素实际触发了被覆盖页面的操作。防御手段是设置 HTTP 响应头X-Frame-Options: DENY或Content-Security-Policy: frame-ancestors none但前端也可以通过frame-busting脚本作为兜底。// frame-busting.ts — 点击劫持防御客户端兜底 /** * 检测当前页面是否被嵌入 iframe 中点击劫持风险 * 注服务端应优先使用 X-Frame-Options / CSP frame-ancestors */ function detectClickjacking(): { embedded: boolean; topOrigin: string | null } { try { // 跨域情况下访问 top.location 会抛出异常 if (window.top window.top ! window.self) { const topOrigin window.top.location.origin; return { embedded: true, topOrigin: topOrigin ! window.location.origin ? topOrigin : null, }; } } catch { // 跨域访问被阻止说明被非可信域名嵌入 return { embedded: true, topOrigin: unknown (cross-origin), }; } return { embedded: false, topOrigin: null }; } /** * 防御性 frame-busting将被劫持的页面跳出 iframe * 仅在检测到异常嵌入时执行 */ function frameBusting() { const { embedded, topOrigin } detectClickjacking(); if (!embedded) return; if (window.self ! window.top) { try { // 记录安全事件 console.warn(检测到点击劫持风险来源:, topOrigin); // 尝试跳出 iframe同源情况下有效 if (window.top) { window.top.location.href window.location.href; } } catch { // 跨域情况下 browser 已阻止访问安全 } } } // 页面加载后执行 if (typeof window ! undefined) { frameBusting(); }四、供应链攻击从 npm 到 CDN 的信任链断点前端供应链攻击的节点包括NPM 依赖投毒、CDN 资源劫持、构建工具插件漏洞、环境变量泄露。防御层次// supply-chain-defense.ts — 前端供应链防御检查 interface SRIHash { url: string; integrity: string; // sha384-... 哈希值 crossorigin?: anonymous | use-credentials; } // 子资源完整性SRI校验配置 // 对 CDN 资源启用 SRI 校验防止 CDN 被劫持后注入恶意代码 const SRI_CONFIG: SRIHash[] [ { url: https://cdn.example.com/react/18.2.0/react.production.min.js, integrity: sha384-...{实际哈希}, // 构建时自动生成 crossorigin: anonymous, }, ]; /** * 验证加载的 script 标签是否包含 SRI 校验 */ function validateSRIScripts(): string[] { const issues: string[] []; const scripts document.querySelectorAll(script[src]); scripts.forEach((script) { const src script.getAttribute(src) ?? ; // 跳过同源脚本 if (!src.startsWith(http)) return; const integrity script.getAttribute(integrity); const crossOrigin script.getAttribute(crossorigin); if (!integrity) { issues.push(CDN 脚本缺少 SRI 校验: ${src}); } if (!crossOrigin script.hasAttribute(integrity)) { issues.push(带 integrity 的脚本缺少 crossorigin 属性: ${src}); } }); return issues; } // 环境变量泄露检测 /** * 检测前端代码中是否泄露了敏感环境变量 */ function detectLeakedEnvVars(envPrefix: string[] [SECRET_, PRIVATE_, API_KEY_]): string[] { const leaked: string[] []; for (const key of Object.keys(import.meta.env ?? {})) { for (const prefix of envPrefix) { if (key.startsWith(prefix)) { leaked.push(敏感环境变量 ${key} 可能在客户端代码中暴露); } } } return leaked; } // 页面加载完成后运行安全检查 document.addEventListener(DOMContentLoaded, () { const sriIssues validateSRIScripts(); if (sriIssues.length 0) { console.warn(SRI 校验问题:, sriIssues); } const envLeaks detectLeakedEnvVars(); if (envLeaks.length 0) { console.warn(环境变量泄露风险:, envLeaks); } });五、总结前端安全的七类威胁——XSS、CSRF、点击劫持、供应链攻击、Cookie 安全、CORS 配置错误、依赖漏洞——背后是同一个防御原则对用户输入零信任对输出内容强制编码对第三方资源完整性校验。防御清单的执行优先级XSS 供应链 CSRF 点击劫持。XSS 的防御成本最低DOMPurify CSP 避免 innerHTML覆盖的攻击面最广供应链攻击的防御成本中等SRI lockfile npm audit但一旦被攻破影响范围最大。建议每季度进行一次安全审计将上述检查脚本集成到 CI 流水线中确保每次提交都经过安全门禁。