前端安全防护体系的全景设计:CSP、SRI、Trusted Types 的深度配置
前端安全防护体系的全景设计:CSP、SRI、Trusted Types 的深度配置
前端安全常被视为"后端的事",但 XSS 攻击的主流入口恰恰是前端——用户输入、URL 参数、第三方脚本。浏览器的安全机制(CSP、SRI、Trusted Types)提供了强大的原生防护能力,但多数项目的配置停留在"开启 report-only 模式"阶段,远未发挥其真正价值。
一、前端安全威胁的全景地图
对应防护层:
| 攻击类型 | 防护机制 | 生效层级 |
|---|---|---|
| XSS | Content-Security-Policy (CSP) | HTTP 响应头 / meta 标签 |
| 脚本完整性 | Subresource Integrity (SRI) | HTML 属性 |
| DOM 注入 | Trusted Types | JavaScript API |
| 点击劫持 | X-Frame-Options / frame-ancestors | HTTP 响应头 |
二、CSP 的深度配置
CSP 是最核心的前端安全策略,但"正确的配置"远比"开启 CSP"复杂。一个过于宽松的 CSP 等同于没有 CSP。
2.1 渐进式 CSP 部署策略
2.2 生产级 CSP 配置
# ===== 第一阶段:Report-Only 模式,仅收集违规报告 ===== Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; report-uri /api/csp-report; # ===== 第二阶段:强制策略,但仍保留部分兼容 ===== Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://cdn.example.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com https://analytics.example.com; frame-src 'self' https://www.youtube.com; upgrade-insecure-requests; report-uri /api/csp-report; # ===== 第三阶段:严格策略,使用 nonce 替代 unsafe-inline ===== Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic'; style-src 'self' 'nonce-{RANDOM}'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; report-uri /api/csp-report;2.3 CSP nonce 的服务端生成
/** * 服务端 CSP nonce 生成中间件 * 每个请求生成唯一的 nonce,防止重放攻击 */ import { randomBytes } from 'crypto'; class CSPMiddleware { /** * 生成 CSP nonce(Base64 编码的 16 字节随机数) */ generateNonce(): string { return randomBytes(16).toString('base64'); } /** * 构建完整 CSP 头 */ buildCSPHeader(nonce: string): string { const directives = [ "default-src 'self'", `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`, `style-src 'self' 'nonce-${nonce}'`, "img-src 'self' data: https:", "font-src 'self'", "connect-src 'self' https://api.example.com", "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'", "report-uri /api/csp-report", ]; return directives.join('; '); } /** * Express/Koa 中间件处理函数 */ middleware(req: any, res: any, next: () => void): void { // 为每个请求生成独立 nonce const nonce = this.generateNonce(); const cspHeader = this.buildCSPHeader(nonce); // 设置响应头 res.setHeader('Content-Security-Policy', cspHeader); // 将 nonce 注入模板上下文,供 HTML 模板使用 res.locals.cspNonce = nonce; next(); } }2.4 CSP 违规报告处理
/** * CSP 违规报告接收端点 */ interface CSPViolationReport { 'csp-report': { 'document-uri': string; 'violated-directive': string; 'blocked-uri': string; 'original-policy': string; 'source-file'?: string; 'line-number'?: number; 'column-number'?: number; }; } /** * CSP 违规报告处理器 */ class CSPReportHandler { private violations: CSPViolationReport[] = []; private deduplicationWindow = new Map<string, number>(); /** * 接收并处理 CSP 违规报告 */ handleReport(report: CSPViolationReport): void { const csp = report['csp-report']; if (!csp) return; // 去重:同一违规 5 分钟内只记录一次 const dedupKey = `${csp['violated-directive']}_${csp['blocked-uri']}_${csp['document-uri']}`; const now = Date.now(); if (this.deduplicationWindow.has(dedupKey)) { const lastReported = this.deduplicationWindow.get(dedupKey)!; if (now - lastReported < 5 * 60 * 1000) { return; // 5 分钟内重复,跳过 } } this.deduplicationWindow.set(dedupKey, now); // 分析违规严重程度 const severity = this.classifyViolation(csp['violated-directive']); // 记录和上报 console.warn(`[CSP Violation ${severity}]`, { directive: csp['violated-directive'], blocked: csp['blocked-uri'], page: csp['document-uri'], source: csp['source-file'], line: csp['line-number'], }); // 持久化或发送到监控系统 this.persistViolation({ ...report, severity, timestamp: now }); } /** * 按违规指令类型分类严重程度 */ private classifyViolation(directive: string): 'critical' | 'warning' | 'info' { if (directive.startsWith('script-src')) return 'critical'; if (directive.startsWith('frame-ancestors')) return 'warning'; return 'info'; } private persistViolation(violation: CSPViolationReport & { severity: string; timestamp: number }): void { // 写入日志系统或发送到监控平台 // 实际生产环境中使用日志聚合系统(如 ELK、Datadog) } }三、SRI:脚本完整性校验
第三方 CDN 脚本可能被篡改或劫持,SRI 通过对脚本内容计算哈希来确保完整性。
3.1 生成 SRI 哈希
# 使用 openssl 计算文件哈希并生成 SRI integrity 属性 cat script.js | openssl dgst -sha384 -binary | openssl base64 -A # 输出: sha384-xxxxx... # 或使用 Node.js 脚本 echo 'console.log("hello")' > /tmp/test.js node -e " const crypto = require('crypto'); const fs = require('fs'); const content = fs.readFileSync('/tmp/test.js'); const hash = crypto.createHash('sha384').update(content).digest('base64'); console.log('sha384-' + hash); "3.2 生产环境使用
<!-- 带 SRI 校验的第三方脚本引入 --> <script src="https://cdn.example.com/analytics/2.5.1/analytics.min.js" integrity="sha384-oqVuAfXRKap7fdgcCY5dn7vic6Ln7VBIxjG3YOvqLVmJYi7GB45eRZCIy4rAGPp" crossorigin="anonymous" defer ></script> <!-- 带 SRI 的 CSS 引入 --> <link rel="stylesheet" href="https://cdn.example.com/ui/1.0.0/theme.css" integrity="sha384-abc123..." crossorigin="anonymous" /> <!-- 多哈希支持(兼容多个安全的版本) --> <script src="https://cdn.example.com/lib/1.x/lib.min.js" integrity="sha384-version1 sha384-version2" crossorigin="anonymous" ></script>3.3 构建时自动生成 SRI
/** * Vite 插件:自动为输出资源生成 SRI 哈希 */ import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; interface SRIOptions { /** 哈希算法 */ algorithm?: 'sha256' | 'sha384' | 'sha512'; /** 输出 SRI 映射文件路径 */ outputFile?: string; } function viteSRI(options: SRIOptions = {}) { const { algorithm = 'sha384', outputFile = 'dist/sri-manifest.json' } = options; const sriMap: Record<string, string> = {}; return { name: 'vite-plugin-sri', enforce: 'post' as const, // 在 bundle 生成后计算哈希 generateBundle(_options: any, bundle: any) { for (const [fileName, chunk] of Object.entries(bundle)) { if (chunk.type === 'chunk' || (chunk.type === 'asset' && /\.(js|css)$/.test(fileName))) { const source = chunk.type === 'chunk' ? chunk.code : (chunk as any).source; if (typeof source !== 'string') continue; const hash = crypto .createHash(algorithm) .update(source) .digest('base64'); sriMap[`/${fileName}`] = `${algorithm}-${hash}`; } } // 输出 SRI 映射文件,供服务端注入 integrity 属性 this.emitFile({ type: 'asset', fileName: path.basename(outputFile), source: JSON.stringify(sriMap, null, 2), }); }, }; } export default viteSRI;四、Trusted Types:从根源防御 DOM XSS
Trusted Types 是 CSP 的补充机制,要求所有注入 DOM 的字符串都必须通过可信类型创建。它是目前防御 DOM XSS 最彻底的方案。
4.1 Trusted Types 策略配置
# CSP 中启用 Trusted Types Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default dompurify;4.2 默认策略实现
/** * Trusted Types 默认策略 * 在启用 Trusted Types 后,所有危险 API 调用都必须通过此策略 */ if (typeof window !== 'undefined' && window.trustedTypes) { // 创建默认策略 const defaultPolicy = window.trustedTypes.createPolicy('default', { /** * 创建可信 HTML * 使用 DOMPurify 清洗输入,确保不会包含恶意脚本 */ createHTML: (input: string) => { // 生产环境必须使用 DOMPurify 等专业清洗库 // 以下为示例,实际项目应引入 DOMPurify return sanitizeHTML(input) as unknown as TrustedHTML; }, /** * 创建可信脚本 URL * 严格白名单校验,只允许受信任的脚本来源 */ createScriptURL: (url: string) => { const allowedDomains = [ 'cdn.example.com', 'static.example.com', 'www.googletagmanager.com', ]; try { const urlObj = new URL(url); if (allowedDomains.some(domain => urlObj.hostname === domain || urlObj.hostname.endsWith(`.${domain}`))) { return url as unknown as TrustedScriptURL; } } catch { // URL 解析失败 } throw new TypeError(`不允许的脚本来源: ${url}`); }, /** * 创建可信脚本 * 默认策略拒绝创建动态脚本,必须使用专用策略 */ createScript: (_script: string) => { throw new TypeError('不允许通过默认策略创建动态脚本'); }, }); } /** * 简单的 HTML 清洗函数(示意,生产环境使用 DOMPurify) */ function sanitizeHTML(html: string): string { // 移除危险标签和属性 const dangerous = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi; const sanitized = html.replace(dangerous, ''); // 移除事件处理器 const events = /\s(on\w+)=/gi; return sanitized.replace(events, '>/** * DOM Purify 专用策略 * 用于 DOMPurify 库的 Trusted Types 集成 */ if (window.trustedTypes) { window.trustedTypes.createPolicy('dompurify', { createHTML: (input: string) => { // DOMPurify.sanitize 返回的本身就是安全的 HTML // 实际项目中引入: import DOMPurify from 'dompurify'; return input as unknown as TrustedHTML; }, }); } /** * 安全的 innerHTML 设置辅助函数 * 替代直接使用 element.innerHTML = xxx */ function safeSetHTML(element: HTMLElement, html: string): void { if (typeof window !== 'undefined' && window.trustedTypes) { // Trusted Types 已启用,使用策略创建 const policy = window.trustedTypes.getPolicy('dompurify') || window.trustedTypes.getPolicy('default'); if (policy) { element.innerHTML = policy.createHTML(html) as unknown as string; return; } } // Trusted Types 未启用,直接设置(降级) element.innerHTML = html; }五、安全头配置的完整清单
一个生产环境应配置的完整 HTTP 安全响应头:
| 响应头 | 作用 | 推荐值 |
|---|---|---|
| Content-Security-Policy | 资源加载白名单 | 严格 nonce 策略 |
| Strict-Transport-Security | 强制 HTTPS | max-age=31536000; includeSubDomains |
| X-Content-Type-Options | 禁止 MIME 嗅探 | nosniff |
| X-Frame-Options | 防点击劫持 | DENY(或用 CSP frame-ancestors) |
| Referrer-Policy | 控制 Referrer 信息 | strict-origin-when-cross-origin |
| Permissions-Policy | 控制浏览器 API 权限 | camera=(), microphone=(), geolocation=() |
/** * 安全响应头中间件 * 统一配置所有安全相关响应头 */ function securityHeadersMiddleware(req: any, res: any, next: () => void): void { // HSTS:强制浏览器使用 HTTPS res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); // 禁止 MIME 类型嗅探 res.setHeader('X-Content-Type-Options', 'nosniff'); // XSS 过滤器(旧版浏览器) res.setHeader('X-XSS-Protection', '0'); // 现代浏览器已弃用,设为 0 避免副作用 // 控制 referrer 信息 res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); // 权限策略 res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(self)'); // 跨域隔离(大项目可选) // res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); // res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); next(); }总结
前端安全防护体系的建设是一个从"开启"到"严格执行"的渐进过程:
- CSP:从 Report-Only 收集基线数据,逐步收紧策略。最终状态是 nonce-based 严格策略,不再依赖
'unsafe-inline'。 - SRI:所有从第三方 CDN 加载的脚本和样式必须附带
integrity属性。在构建流程中自动生成哈希值,避免手动维护。 - Trusted Types:从根源杜绝 DOM XSS——要求所有注入 DOM 的字符串必须通过可信类型策略。配合 DOMPurify 等清洗库,将 XSS 攻击面降到最低。
- 其他安全头:HSTS、X-Content-Type-Options、Referrer-Policy 等作为防御深度的一环。
安全配置的难点不在于"知道该怎么做",而在于"在业务迭代中持续执行"。建议将 CSP 违规率、SRI 覆盖率纳入团队的代码质量看板,作为 CI 的一道检查门禁。
