前端安全
XSS(Cross-Site Scripting)
类型对比
| 类型 |
触发方式 |
持久性 |
示例场景 |
| 存储型 XSS |
恶意代码经服务端存储在数据库中,其他用户访问时渲染执行 |
持久 |
评论区、用户资料、富文本编辑器、留言板 |
| 反射型 XSS |
恶意代码在请求 URL 参数中,服务端不经处理直接反射到页面 |
非持久(需诱导用户点击链接) |
搜索页显示搜索词、错误页面回显参数 |
| DOM 型 XSS |
客户端 JavaScript 在处理过程中将不可信数据插入 DOM |
非持久 |
innerHTML、document.write、eval、location.hash |
// DOM 型 XSS 示例 - 危险代码
const name = new URLSearchParams(location.search).get('name');
document.getElementById('welcome').innerHTML = `欢迎,${name}!`;
// 攻击: ?name=<img src=x onerror=alert('XSS')>
// ✅ 安全的写法: 使用 textContent 而非 innerHTML
document.getElementById('welcome').textContent = `欢迎,${name}!`;
XSS 防御体系
| 防御层 |
手段 |
说明 |
| 输出转义 |
HTML 实体化 |
< → <、" → " 等 |
| 模板安全 |
使用自动转义的模板引擎 |
React JSX 默认转义、Vue 模板默认转义、{{{ }}} 手动开启 |
| CSP |
Content-Security-Policy 头 |
限制可加载和执行的内容源 |
| Cookie 安全 |
HttpOnly 标志 |
禁止 JavaScript 读取 Cookie(防御 XSS 窃取会话) |
| 输入校验 |
白名单机制 |
只允许预期格式的输入(而非黑名单) |
| HTML 净化 |
DOMPurify 库 |
安全地在 DOM 中插入用户生成的 HTML |
CSP(Content Security Policy)
# 严格 CSP 配置(推荐)
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';
| CSP 指令 |
控制内容 |
关键值 |
default-src |
所有资源的默认源 |
'self' |
script-src |
可执行的脚本来源 |
'self' 'nonce-{random}' 'strict-dynamic'(推荐严格 CSP) |
style-src |
样式表来源 |
'self' 'unsafe-inline' |
img-src |
图片来源 |
'self' https: data: |
connect-src |
XMLHttpRequest/Fetch/WebSocket 目标 |
'self' |
frame-ancestors |
允许嵌入本页的父页面(防点击劫持) |
'none' |
report-uri / report-to |
违规上报地址 |
/csp-report |
DOMPurify 使用
import DOMPurify from 'dompurify';
const dirty = '<img src=x onerror=alert("xss")><p>hello</p>';
const clean = DOMPurify.sanitize(dirty);
// 结果: <p>hello</p> (移除恶意标签和事件处理)
CSRF(Cross-Site Request Forgery)
原理
用户已登录 bank.com(Cookie 已设置)
│
▼
用户访问 evil.com ──► 自动向 bank.com/transfer?to=attacker&amount=10000 发请求
│
▼
Cookie 自动携带 ──► 服务端收到合法请求(以为是用户本人)
- 跨域请求自动携带 Cookie(包括 session cookie)
- 攻击者无法读取响应,但能执行写操作(转账、改密)
防御方案对比
| 防御手段 |
机制 |
安全等级 |
说明 |
| SameSite Cookie |
浏览器控制 Cookie 是否在跨站请求中发送 |
强(Lax/Strict) |
最推荐,作为第一道防线 |
| CSRF Token |
服务端生成随机 token,每次请求验证 |
强 |
需前后端配合,额外逻辑 |
| Referer/Origin 校验 |
检查请求头的 Referer/Origin 是否在白名单 |
中 |
Referer 可能被屏蔽或篡改 |
| 二次确认 |
敏感操作要求输入密码/验证码 |
最强 |
用户体验差 |
SameSite Cookie
| 模式 |
跨站链接点击 |
跨站表单提交(POST) |
跨站请求(GET) |
Strict |
不发送 |
不发送 |
不发送 |
Lax(默认) |
发送 |
不发送 |
发送(顶级导航) |
None |
发送 |
发送 |
发送(需配合 Secure) |
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax
Set-Cookie: CSRF-Token=xyz789; HttpOnly; Secure; SameSite=Strict
Chrome 80+ 默认 SameSite=Lax,必须手动设置 SameSite=None; Secure 才能跨站发送。
CSRF Token 流程
// 服务端生成 token 嵌入页面
<meta name="csrf-token" content="random-token-123">
// 前端每次请求携带
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: ...
});
点击劫持(Clickjacking)
原理
- 攻击者使用透明 iframe 覆盖在诱饵页面(按钮)上
- 用户实际点击的是透明 iframe 中的目标页面(如"点赞"、"转账")
防御
# 方法 1:X-Frame-Options(旧,支持度广)
X-Frame-Options: DENY # 禁止任何 iframe 加载
X-Frame-Options: SAMEORIGIN # 仅允许同域名 iframe 加载
# 方法 2:CSP frame-ancestors(更灵活,推荐现代浏览器)
Content-Security-Policy: frame-ancestors 'none'; # 禁止任何
Content-Security-Policy: frame-ancestors 'self' https://trusted.com; # 白名单
| 手段 |
支持度 |
粒度 |
备注 |
X-Frame-Options: DENY |
所有浏览器 |
粗(全部禁止) |
旧标准,适用于简单场景 |
CSP frame-ancestors |
现代浏览器 |
细(可指定多个域名) |
更灵活,支持多域名白名单 |
JS 防框架(if(top!=self)) |
所有 |
客户端 |
可被绕过(noopener 等),不推荐单独使用 |
CORS 配置安全
| 配置 |
安全风险 |
推荐做法 |
Access-Control-Allow-Origin: * |
任意域可读响应数据(如 API 返回敏感信息) |
校验收到的 Origin 头并回写具体域名 |
Access-Control-Allow-Credentials: true + * |
浏览器拒绝这种组合(规范规定) |
指定具体 Origin + Credentials |
| 未校验 Origin |
任意站点可跨域请求 |
白名单匹配 Origin |
# ❌ 不安全
Access-Control-Allow-Origin: *
# ✅ 安全
Access-Control-Allow-Origin: https://your-app.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Vary: Origin
使用 Vary: Origin 让 CDN 根据 Origin 缓存不同的响应。
HTTPS 与传输安全
HSTS(HTTP Strict Transport Security)
# 严格配置
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
| 参数 |
含义 |
max-age=63072000 |
缓存 2 年(单位秒) |
includeSubDomains |
所有子域名也强制 HTTPS |
preload |
提交到浏览器预加载列表(硬编码到浏览器,即使首次访问也是 HTTPS) |
- HSTS 生效后浏览器自动将 HTTP 转为 HTTPS
- 首次访问风险:浏览器尚未收到 HSTS 头 → 中间人可以拦截 → 解决:HSTS Preload
证书透明(Certificate Transparency)
- 所有公开 CA 签发的证书必须记录到公共 CT 日志
- 浏览器验证证书是否存在于 CT 日志中
- 防止 CA 被攻破后签发伪造证书而不被发现
依赖安全
npm audit
# 扫描已知漏洞
npm audit
# 修复(自动升级可修复的依赖)
npm audit fix
# 查看详情
npm audit --json
lockfile 固定版本
# package-lock.json 或 yarn.lock 必须提交到仓库
# 确保 CI/CD 和生产环境安装的依赖版本一致
供应链攻击类型
| 攻击类型 |
描述 |
案例 |
| 依赖混淆 |
攻击者在公共仓库发布同名包,当内部包名被拼写错误时自动安装恶意公共包 |
event-stream(2018) |
| 恶意包劫持 |
维护者账号被黑或钓鱼,在合法包中植入恶意代码 |
ua-parser-js / coa / rc(2021) |
| 原型污染 |
在第三方库中写入 __proto__ 属性 |
lodash(CVE-2019-10744) |
防御措施:
npm audit / npm outdated 定期检查
- 使用
socket.dev 或 snyk 进行依赖安全扫描
- 设置
npm config set registry 指定私有 registry 优先
- 使用
--ignore-scripts 安装 + 手动检查 postinstall 脚本
SRI(Subresource Integrity)
<!-- CDN 引入时校验文件哈希 -->
<script
src="https://cdn.example.com/react@18/react.production.min.js"
integrity="sha384-..."
crossorigin="anonymous"
></script>
integrity 属性指定 base64 编码的哈希值(sha384/sha512)
- 浏览器下载后计算哈希对比,不匹配则拒绝执行
- 必须配合
crossorigin 属性
认证与授权
JWT(JSON Web Token)
结构: header.payload.signature
// header: 算法类型
{ "alg": "HS256", "typ": "JWT" }
// payload: 声明
{ "sub": "user123", "iat": 1516239022, "exp": 1516242622 }
// signature: 对 header + payload 的签名
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
Token 存储位置对比
| 存储位置 |
XSS 风险 |
CSRF 风险 |
可用性 |
| httpOnly Cookie |
低(JS 无法读取) |
需 CSRF 防御(SameSite + Token) |
自动发送 |
| localStorage |
高(XSS 可窃取) |
低(不会自动发送) |
需 JS 手动添加 Authorization 头 |
| 内存变量 |
低 |
低 |
刷新页面丢失 |
推荐: accessToken 短时效(15min)存内存 + refreshToken 长时效(7天)存 httpOnly Cookie
双 Token 刷新策略
// 请求拦截器:自动检查 token 过期
let isRefreshing = false;
let refreshSubscribers = [];
axios.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401 && !error.config._retry) {
if (isRefreshing) {
// 队列等待刷新完成
return new Promise(resolve => {
refreshSubscribers.push(token => {
error.config.headers.Authorization = `Bearer ${token}`;
resolve(axios(error.config));
});
});
}
error.config._retry = true;
isRefreshing = true;
const { token } = await axios.post('/refresh');
isRefreshing = false;
error.config.headers.Authorization = `Bearer ${token}`;
refreshSubscribers.forEach(cb => cb(token));
refreshSubscribers = [];
return axios(error.config);
}
return Promise.reject(error);
}
);
Token 刷新策略对比
| 策略 |
描述 |
优点 |
缺点 |
| 双 Token |
accessToken(15min)+ refreshToken(7天) |
短 token 被盗损失小 |
需额外刷新逻辑 |
| 滑动窗口 |
每次请求时将 token 过期时间后延 |
用户体验好 |
刷新频繁,服务器压力大 |
| 单 Token 短时效 |
token 15 分钟过期 |
简单 |
用户频繁重新登录 |
OAuth 2.0 流程
| 模式 |
适用场景 |
安全注意 |
| 授权码模式(Authorization Code) |
服务端应用 |
最安全,code 只能使用一次 |
| 授权码 + PKCE |
单页应用 / 移动端 |
无需 client_secret,用 code_challenge 替换 |
| Implicit |
已被 OAuth 2.1 废弃 |
token 在 URL 中暴露,不安全 |
| Client Credentials |
服务端到服务端 |
无用户参与 |
PKCE(Proof Key for Code Exchange): 客户端生成 code_verifier + code_challenge(SHA256),授权码换 token 时校验,即使授权码被拦截也无法换 token。
Session 管理
| 方案 |
存储位置 |
扩展性 |
缺点 |
| 服务端 sessionId + Cookie |
服务端内存/Redis |
需集中存储 |
水平扩展需共享 session 存储 |
| JWT |
客户端 |
好(无状态) |
无法主动注销 |
其他安全风险
原型污染(Prototype Pollution)
// 攻击示例
const obj = {};
obj.__proto__.isAdmin = true;
// 或通过 lodash merge 等深拷贝方法
_.merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
// 防御
// 1. 使用无原型对象
const safe = Object.create(null);
// 2. 冻结原型
Object.freeze(Object.prototype);
// 3. 校验 JSON key
function sanitize(obj) {
delete obj.__proto__;
delete obj.constructor;
return obj;
}
SSRF(Server Side Request Forgery)
- 攻击者控制服务端发起的内网请求,访问内部资源
- 防御: 白名单 URL、禁止对内网 IP 发起请求、限制协议
ReDoS(正则拒绝服务)
// 危险正则 - 灾难性回溯
const evil = /^(a+)+b$/;
evil.test('aaaaaaaaaaaaaaaaaaaaaac'); // 指数级耗时
// 防御
// 1. 避免嵌套量词(`(a+)+` / `(a|aa)+` / `(.*)*`)
// 2. 使用 `re2` 库(拒绝回溯机制的正则引擎)
// 3. 设置正则匹配超时