前端安全不是「后端的事」。 当 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 Types | DOM-based |
| 架构层 | 框架自动转义(Vue/React) | 全部 |
二、XSS 防御实战
2.1 Vue / React 的自动转义
<!-- Vue:{{ }} 自动转义 HTML -->
<template>
<!-- 用户输入 <script>alert(1)</script> -->
<div>{{ userInput }}</div>
<!-- 输出:<script>alert(1)</script> — 安全! -->
<!-- ❌ 危险: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-src | JS 来源 | 'self' 'nonce-xxx' |
style-src | CSS 来源 | 'self' 'unsafe-inline' |
img-src | 图片来源 | 'self' data: https: |
connect-src | fetch/XHR/WebSocket | 'self' https://api.example.com |
font-src | 字体来源 | 'self' https://fonts.gstatic.com |
frame-src | iframe | 'self' |
media-src | 音视频 | 'self' |
object-src | <object> | 'none' |
base-uri | <base> 目标 | 'self' |
form-action | 表单提交 | 'self' |
upgrade-insecure-requests | HTTP→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 | 点击劫持防护 | DENY 或 SAMEORIGIN |
Referrer-Policy | 控制 Referer 泄漏 | strict-origin-when-cross-origin |
Permissions-Policy | 限制浏览器 API | camera=(), microphone=() |
Strict-Transport-Security | 强制 HTTPS | max-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 | 必须 | 代码 |
参考与延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。