前端安全加固
前端安全不是后端安全的附属品——浏览器里的每一行代码都暴露在攻击者的火力范围内。本文按威胁类型逐一讲解 XSS、CSRF、依赖漏洞、点击劫持、供应链攻击的攻防细节,最后给出一份可对照执行的安全编码检查清单。
一、前端安全威胁全景
| 威胁 | 攻击方式 | 后果 | 主要防线 |
|---|---|---|---|
| XSS 跨站脚本 | 注入可执行脚本 | 窃取 Cookie、会话劫持、钓鱼 | 输出编码、CSP |
| CSRF 跨站请求伪造 | 诱导浏览器发请求 | 以用户身份执行操作 | SameSite、Token 校验 |
| 点击劫持 | 透明 iframe 覆盖 | 诱导点击完成转账等操作 | X-Frame-Options、CSP frame-ancestors |
| 依赖漏洞 | 恶意/带漏洞的第三方包 | 数据泄露、远程执行 | npm audit、版本锁定 |
| 供应链攻击 | 投毒包、被攻破的发布账号 | 上线即被控制 | 锁定版本、签名校验、最小依赖 |
| 敏感信息泄露 | 硬编码密钥、日志泄漏 | 账号体系被攻破 | 密钥走环境变量,绝不入库 |
二、XSS 深入防御
1. 攻击类型与注入点
| 类型 | 注入位置 | 示例 |
|---|---|---|
| 存储型 | 持久化数据(评论、昵称) | 提交 <img src=x onerror=alert(1)> 被保存 |
| 反射型 | URL 参数回显 | ?q=<script>...</script> 直接拼进页面 |
| DOM 型 | JS 操作 DOM 时拼接 | innerHTML = location.hash 中的恶意片段 |
2. 第一道防线:输出编码
所有插入 HTML 的用户输入,必须先转义:
// 通用转义函数:把危险字符替换为实体
function escapeHtml(str) {
return String(str)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// 差:直接拼接用户输入
element.innerHTML = `<p>${userComment}</p>`;
// 好:转义后再拼接
element.innerHTML = `<p>${escapeHtml(userComment)}</p>`;
// 更优:优先用 textContent,完全不给 HTML 解析机会
element.textContent = userComment;3. 第二道防线:框架自动转义
| 框架 | 默认行为 | 风险 API | 正确用法 |
|---|---|---|---|
| Vue | 插值 自动转义 | v-html | 非可信内容禁止使用 v-html |
| React | JSX 文本自动转义 | dangerouslySetInnerHTML | 仅用于可信静态内容,并配合净化 |
| 原生 DOM | 无默认保护 | innerHTML、outerHTML | 用 textContent/createTextNode 替代 |
<!-- Vue:v-html 会原样渲染 HTML,输入不可信时等于敞开 XSS 大门 -->
<div v-html="userComment"></div>
<!-- React:dangerouslySetInnerHTML 同理 -->
<div dangerouslySetInnerHTML={{ __html: userComment }} />// 确需渲染富文本时,先做白名单净化(如 DOMPurify)
import DOMPurify from "dompurify";
element.innerHTML = DOMPurify.sanitize(userComment, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a"],
ALLOWED_ATTR: ["href"],
});4. 第三道防线:CSP 白名单
即使注入发生,CSP 也能阻止脚本执行(详见"五、CSP 配置实践")。
5. 其他输入途径也要防
// URL 注入:javascript: 协议
const url = userInput;
if (/^https?:\/\//.test(url)) location.href = url; // 只允许 http/https
// eval 与 new Function:等同直接执行注入代码,业务代码中应彻底避免三、CSRF 防御实践
1. 攻击原理
用户在登录状态下访问了恶意网站,恶意页面用自动提交的表单/图片让浏览器带上用户 Cookie 向目标站发出请求,服务端误以为是本人操作。
2. 第一层:SameSite 属性
Cookie 设置 SameSite 后,跨站请求默认不再携带:
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax| SameSite 值 | 跨站发送 Cookie | 适用 |
|---|---|---|
Strict | 一律不发送 | 最高安全,牺牲部分跳转体验 |
Lax | 顶层导航 GET 发送 | 默认推荐(现代浏览器默认值) |
None(需配合 Secure) | 全部发送 | 必须跨站携带的场景(SSO) |
3. 第二层:Token 方案
服务端签发随机 Token,前端在每次请求头携带,服务端校验——攻击者读不到也猜不到这个值,伪造的请求自然失败:
// 登录后从接口获取 csrf token,之后所有请求带上
const csrfToken = await fetch("/api/csrf").then(r => r.json());
fetch("/api/transfer", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken.token, // 自定义头:跨站请求无法主动设置
},
body: JSON.stringify({ to: "bob", amount: 1000 }),
});4. 第三层:请求头校验
服务端校验 Origin/Referer 是否为白名单域名。由于浏览器跨站发起的请求无法携带自定义头,仅要求 X-Requested-With 等自定义头也能拦截绝大多数 CSRF。
| 防线 | 位置 | 力度 |
|---|---|---|
| SameSite Cookie | 浏览器 | 基础防线,默认开 |
| Token 校验 | 应用层 | 强,防伪造 |
| Origin/Referer 校验 | 服务端 | 辅助 |
四、依赖安全
1. npm audit 漏洞扫描
npm audit # 列出漏洞及修复建议
npm audit fix # 自动修复(升小版本)
npm audit fix --force # 大版本升级,可能破坏兼容,需评估
npm audit --json # 输出 JSON 供 CI 解析2. 漏洞库与工具链
| 工具/平台 | 作用 |
|---|---|
| npm audit(OSV 数据) | 依赖漏洞扫描 |
| GitHub Dependabot | 自动创建修复 PR |
| Snyk / Sonatype | 深度漏洞情报与许可检查 |
| Renovate | 自动依赖升级机器人 |
3. 版本锁定
{
"dependencies": {
"lodash": "4.17.21" // 精确锁定,避免小版本漂移
}
}- 生产依赖用
lockfile(package-lock.json/pnpm-lock.yaml)提交入库,保证安装结果可复现。 - 升级大版本前先看 CHANGELOG 与安全公告,不要无脑
npm update。
五、CSP 配置实践
CSP(内容安全策略)通过响应头声明"允许加载什么",让 XSS 注入的脚本无源可执行。
1. 指令组合
Content-Security-Policy:
default-src 'self'; # 默认只信同源
script-src 'self' cdn.example.com; # 脚本只允许同源 + 指定 CDN
style-src 'self' 'unsafe-inline'; # 样式允许内联(写行内样式常见)
img-src 'self' data: https:; # 图片允许 data: 与任意 https
connect-src 'self' https://api.example.com; # 网络请求白名单
frame-ancestors 'none'; # 禁止被任何页面 iframe 嵌套(防点击劫持)
report-uri /csp-report; # 违规上报地址| 常用指令 | 控制内容 |
|---|---|
default-src | 未单独声明时的兜底 |
script-src | 脚本来源(最关键) |
style-src | 样式来源 |
img-src / font-src / media-src | 各类资源来源 |
connect-src | fetch/XHR/WebSocket 目标 |
frame-ancestors | 允许嵌套本页的父页面 |
2. nonce:可信内联脚本
需要内联脚本时用一次性 nonce 放行,攻击者注入的脚本无法预知 nonce:
<!-- 服务端每次生成随机 nonce -->
<script nonce="aB3xY9...">initApp();</script>Content-Security-Policy: script-src 'self' 'nonce-aB3xY9...'3. report-only:灰度验证
先用只上报不拦截的模式验证策略不会误伤业务,再切正式:
Content-Security-Policy-Report-Only:
default-src 'self'; report-uri /csp-report;六、输入校验与数据净化
原则:所有来自用户的输入都是不可信的,在客户端与服务端双重校验。
// 白名单校验优先于黑名单过滤
const EMAIL_RE = /^[\w.+-]+@[\w-]+\.[\w.-]+$/;
function isValidEmail(email) {
return typeof email === "string" && EMAIL_RE.test(email) && email.length <= 254;
}
// 数字范围与长度限制
function sanitizeAmount(input) {
const n = Number(input);
return Number.isFinite(n) && n > 0 && n <= 100000 ? n : null;
}
// 长度上限防超大请求
function truncateText(text, max = 200) {
return String(text).slice(0, max);
}注意:客户端校验只负责体验,安全校验必须在服务端执行——攻击者可以直接绕过浏览器。
七、点击劫持与 UI 安全
1. 点击劫持原理
攻击者在自己的页面里用透明 iframe 嵌入目标网站,诱导用户点击"看不见的按钮",实际点到了目标站的转账/关注按钮。
2. 防御手段
# 响应头双重防护
X-Frame-Options: DENY # 老方案:DENY / SAMEORIGIN
Content-Security-Policy: frame-ancestors 'none'; # 新方案,更细粒度// 前端兜底:检测被嵌套则强制跳出(只能作为辅助,不可依赖)
if (window.top !== window.self) {
window.top.location = window.self.location;
}3. UI 欺骗防护
- 弹窗钓鱼(Windows 弹窗克隆)、视觉欺骗:对敏感操作增加二次确认(输入密码/验证码)。
- 关键操作(支付、改密)强制走独立页面而不是弹层,减少被覆盖的可能性。
八、供应链攻击防护
| 攻击路径 | 案例 | 防护 |
|---|---|---|
| 恶意包投毒 | 仿冒知名包名(lodahs) | 安装前核对包名与来源;用 npm view 检查 |
| 被攻破的维护者账号 | 流行库被植入后门 | 锁定精确版本、关注安全公告 |
| 依赖中的依赖 | 深层依赖含漏洞 | 定期 npm audit 全覆盖 |
| 开发机被入侵 | 本地安装的全局工具被替换 | 最小权限、密钥管理 |
落地措施:
npm install --save-exact package-name # 精确版本安装
npm audit --audit-level=high # CI 中高危漏洞直接失败
npm ci # CI 中严格按 lockfile 安装- 最小化依赖:一个功能自己能写就别引库,依赖越少攻击面越小。
- 密钥安全:Token/密钥放环境变量或密钥管理服务,严禁提交到代码仓库;前端要暴露的密钥(如地图 Key)单独管理并加域名白名单。
九、安全编码检查清单
| 检查项 | 通过标准 |
|---|---|
| 输出编码 | 所有用户输入插入 HTML 前已转义或使用 textContent |
| 框架 API | 无可信依据不使用 v-html/dangerouslySetInnerHTML/innerHTML 拼接 |
| eval 使用 | 代码库中不存在 eval/new Function 执行动态代码 |
| CSP | 已配置 script-src 白名单并验证无漏放 |
| Cookie 属性 | 会话 Cookie 带 HttpOnly、Secure、SameSite |
| CSRF 防护 | 状态变更请求带 Token 或服务端校验 Origin |
| 点击劫持 | 已设置 frame-ancestors/X-Frame-Options |
| 依赖漏洞 | npm audit 无高危及以上漏洞,lockfile 已入库 |
| 密钥管理 | 仓库中无明文密钥,密钥走环境变量 |
| 输入校验 | 用户输入有白名单校验且服务端二次校验 |
安全加固的最终形态不是某一次修复,而是把上述检查项固化到流程里:提交前自查、CI 中跑 npm audit 与安全扫描、上线前对照清单评审。每一条防线都可能被绕过,但层层叠加之后,攻击者的成本会高到不值得为止。