前端安全进阶:XSS、CSP、SRI 与供应链安全

系统性前端安全深度指南:XSS 全分类(Reflected/Stored/DOM-based/mXSS)与防御矩阵(转义/CSP/DOMPurify/Trusted Types)、CSP(Content Security Policy)策略设计与部署、SRI(Subresource Integrity)校验、CSRF Token 与 SameSite Cookie、前端加密与密钥管理、npm 供应链安全(package-lock/audit/SBOM/openSSF Scorecard)、原型污染防御、eval/new Function 沙箱替代方案、浏览器安全头全览。附 Vue/React/Next.js 安全配置。

前端安全不是「后端的事」。 当 XSS 脚本在用户浏览器中执行时,它已经绕过了所有后端防御。前端是第一道防线,也是最后一公里——用户看到什么、执行什么,最终由前端代码决定。


一、XSS(跨站脚本攻击)全分类

1.1 三种攻击模式

XSS 攻击路径:

Reflected XSS(反射型):
  攻击者发送链接 → 用户点击 → 恶意脚本在 URL 参数中 → 服务端返回含脚本的页面 → 执行
  例:https://vuln.com/search?q=<script>alert(document.cookie)</script>

Stored XSS(存储型):
  攻击者提交恶意内容 → 存入数据库 → 其他用户浏览时执行
  例:评论区输入 <img src=x onerror="fetch('/steal?c='+document.cookie)">

DOM-based XSS(DOM 型):
  恶意输入不经过服务端,直接由前端 JS 处理并插入 DOM
  例:location.hash.slice(1) → innerHTML 赋值(未转义)

mXSS(突变型):
  浏览器 HTML 解析器在序列化/反序列化时改变内容,绕过过滤器
  例:<a title="--><script>alert(1)</script>">

1.2 防御矩阵

防御层手段防御类型
输入层验证 + 白名单Reflected, Stored
输出层HTML 实体转义全部
协议层CSP(Content Security Policy)全部
DOM 层DOMPurify + Trusted TypesDOM-based
架构层框架自动转义(Vue/React)全部

二、XSS 防御实战

2.1 Vue / React 的自动转义

<!-- Vue{{ }} 自动转义 HTML -->
<template>
  <!-- 用户输入 <script>alert(1)</script> -->
  <div>{{ userInput }}</div>
  <!-- 输出&lt;script&gt;alert(1)&lt;/script&gt; — 安全! -->

  <!--  危险v-html 绕过转义 -->
  <div v-html="userInput"></div>

  <!--  必须用 v-html 时先净化 -->
  <div v-html="sanitizedHtml"></div>
</template>

<script setup>
import DOMPurify from 'dompurify';
const sanitizedHtml = DOMPurify.sanitize(userInput);
</script>
// React:{} 自动转义 JSX
function Profile({ bio }) {
  return (
    <div>
      {bio}  {/* 自动转义 — 安全 */}
      {/* <div dangerouslySetInnerHTML={{ __html: bio }} />  ← 危险! */}
    </div>
  );
}

2.2 DOMPurify:可配置的 HTML 净化

import DOMPurify from 'dompurify';

// 基础配置
const clean = DOMPurify.sanitize(dirtyHtml);

// 安全配置(富文本编辑器场景)
const config = {
  ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li', 'h1', 'h2', 'h3'],
  ALLOWED_ATTR: ['href', 'title', 'target'],
  ALLOW_DATA_ATTR: false,
  SANITIZE_DOM: true,
  // 强制链接 noopener
  ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|xxx):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i
};

const safeHtml = DOMPurify.sanitize(dirtyHtml, config);

2.3 Trusted Types(浏览器原生防御)

// 注册 Trusted Types 策略
if (window.trustedTypes && window.trustedTypes.createPolicy) {
  window.trustedTypes.createPolicy('default', {
    createHTML: (input) => DOMPurify.sanitize(input),
    createScriptURL: (input) => {
      const allowedDomains = ['cdn.example.com', 'cdn.jsdelivr.net'];
      const url = new URL(input, location.href);
      if (!allowedDomains.includes(url.hostname)) {
        throw new TypeError(`Blocked script URL: ${input}`);
      }
      return input;
    }
  });
}

// CSP 中强制 Trusted Types
// Content-Security-Policy: require-trusted-types-for 'script'

三、CSP:Content Security Policy

3.1 CSP 指令速查

指令控制内容示例
default-src默认回退'self'
script-srcJS 来源'self' 'nonce-xxx'
style-srcCSS 来源'self' 'unsafe-inline'
img-src图片来源'self' data: https:
connect-srcfetch/XHR/WebSocket'self' https://api.example.com
font-src字体来源'self' https://fonts.gstatic.com
frame-srciframe'self'
media-src音视频'self'
object-src<object>'none'
base-uri<base> 目标'self'
form-action表单提交'self'
upgrade-insecure-requestsHTTP→HTTPS

3.2 分阶段部署策略

阶段 1:Report-Only 模式(只上报不拦截)
  Content-Security-Policy-Report-Only:
    default-src 'self';
    report-uri https://csp.example.com/report;

阶段 2:收紧 script-src(最危险)
  Content-Security-Policy:
    default-src 'self';
    script-src 'self' 'nonce-xxx';

阶段 3:逐步收紧其他指令
  Content-Security-Policy:
    default-src 'self';
    script-src 'self';
    style-src 'self' 'nonce-xxx';
    img-src 'self' data: https:;
    connect-src 'self' https://api.example.com;
    object-src 'none';
    base-uri 'self';
    form-action 'self';

3.3 Next.js CSP 配置

// next.config.js
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');

const cspHeader = `
  default-src 'self';
  script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
  style-src 'self' 'nonce-${nonce}';
  img-src 'self' blob: data:;
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests;
`;

module.exports = {
  async headers() {
    return [{
      source: '/(.*)',
      headers: [{
        key: 'Content-Security-Policy',
        value: cspHeader.replace(/\s+/g, ' ').trim()
      }]
    }];
  }
};

3.4 Nginx CSP 配置

# nginx.conf
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$csp_nonce'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';" always;

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 Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

四、SRI:Subresource Integrity

防止 CDN 资源被篡改,通过哈希校验确保内容一致。

<!-- CDN 资源添加 integrity 属性 -->
<script
  src="https://cdn.jsdelivr.net/npm/vue@3.4.0/dist/vue.global.prod.js"
  integrity="sha384-abc123..."
  crossorigin="anonymous"
></script>

<!-- 使用 SRI Hash 生成工具 -->
<!-- https://www.srihash.org/ -->
<!-- 或命令行:
  curl -s https://cdn.example.com/lib.js | openssl dgst -sha384 -binary | openssl base64 -A
-->

4.1 Webpack 自动 SRI

// webpack.config.js
const SriPlugin = require('webpack-subresource-integrity');

module.exports = {
  plugins: [
    new SriPlugin({
      hashFuncNames: ['sha384', 'sha512'],
      enabled: process.env.NODE_ENV === 'production'
    })
  ]
};

五、Cookie 安全

Set-Cookie: session=xxx;
  HttpOnly;      // 禁止 JS 访问(防 XSS 窃取)
  Secure;        // 仅 HTTPS 传输
  SameSite=Strict; // 完全禁止跨站携带(CSRF 防御最强)
  Path=/;
  Max-Age=86400
SameSite 值行为场景
Strict任何跨站请求都不带 Cookie高安全性(银行)
Lax导航链接(GET)可带,POST/iframe 不可✅ 推荐默认
None全部请求都带(需 Secure)跨站 iframe/embed

六、原型 pollution 防御

// ❌ 危险:递归合并不检查 __proto__
function mergeDeep(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object') {
      mergeDeep(target[key], source[key]); // key 可能为 "__proto__"
    } else {
      target[key] = source[key];
    }
  }
}

// 攻击:
// mergeDeep({}, JSON.parse('{ "__proto__": { "isAdmin": true } }'))
// 所有对象都成了 isAdmin: true

// ✅ 安全:冻结 Object.prototype
Object.freeze(Object.prototype);
Object.freeze(Array.prototype);

// ✅ 安全:递归合并时拒绝危险 key
const DANGEROUS_KEYS = ['__proto__', 'constructor', 'prototype'];
function safeMerge(target, source) {
  for (const key in source) {
    if (DANGEROUS_KEYS.includes(key)) continue;
    // ...
  }
}

七、npm 供应链安全

7.1 工具链

# 检查已知漏洞
npm audit
npm audit fix
npm audit fix --force  # ⚠️ 可能引入 breaking change

# 升级检查
npm outdated

# lockfile 验证(防篡改)
npm ci  # 严格按 lockfile 安装

# SBOM(软件物料清单)
npm sbom

# 依赖分析
dependency-check --project my-app --scan ./

# Snyk(更全面的扫描)
npx snyk test
npx snyk monitor

7.2 防护策略

策略做法
锁定版本提交 package-lock.json / pnpm-lock.yaml
定期审计CI 中运行 npm audit
最小依赖减少攻击面,定期清理无用依赖
私有 Registry企业级用 Verdaccio / Nexus
签名验证npm audit signatures
隔离运行Docker、沙箱

八、浏览器安全头全览

头部目的推荐值
Content-Security-Policy资源加载白名单见上文
X-Content-Type-Options禁止 MIME 嗅探nosniff
X-Frame-Options点击劫持防护DENYSAMEORIGIN
Referrer-Policy控制 Referer 泄漏strict-origin-when-cross-origin
Permissions-Policy限制浏览器 APIcamera=(), microphone=()
Strict-Transport-Security强制 HTTPSmax-age=31536000; includeSubDomains; preload
Cross-Origin-Opener-Policy跨窗口隔离same-origin
Cross-Origin-Embedder-Policy跨域嵌入策略require-corp

九、前端安全 Checklist

检查项等级验证
CSP 部署(至少 default-src + script-src)必须headers
v-html / dangerouslySetInnerHTML 被 DOMPurify 净化必须代码审查
Cookie HttpOnly + Secure + SameSite必须headers
SRI(CDN 资源)推荐HTML
npm audit 无高危漏洞必须CI
Object.prototype 已 freeze推荐代码
eval / new Function 被禁止(CSP)推荐CSP
安全头全部部署推荐headers
输入验证(白名单)必须代码
富文本编辑器使用 DOMPurify必须代码

参考与延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章

  1. API 设计与 BFF 层:REST、GraphQL、tRPC 选型与前后端协作
  2. WebAssembly 前端工程化实践:编译链、性能对比与混合架构
  3. 现代浏览器 API 与 Web 平台能力地图