Skip to content

08 · 前端安全 ​

本篇解决什么问题: 前端安全常被误解为"后端的事"。但 XSS、CSRF、依赖投毒、Token 存储这些 都直接发生在前端,而且出了事往往先从前端爆。 本篇按攻击类型 → 原理 → 防御组织,每条都给可落地的代码。

📌 核心原则:前端的一切输入都不可信,前端的一切校验都可绕过——安全边界必须在服务端。


开篇速览 ​

威胁一句话关键防御
XSS把恶意脚本注入页面执行转义输出 + CSP + 避免 innerHTML
CSRF诱导用户在已登录状态下发请求SameSite Cookie + Token/双重验证
点击劫持透明 iframe 覆盖按钮X-Frame-Options / CSP frame-ancestors
依赖供应链第三方包被投毒lockfile + 审计 + 最小依赖
Token 存储Token 放哪才不容易被盗优先 HttpOnly Cookie
传输安全中间人窃听/篡改全站 HTTPS + HSTS

一、XSS(跨站脚本攻击) ​

原理:把恶意 JS 注入到页面,在受害者浏览器里执行。

1.1 三种类型 ​

类型注入位置触发方式例子
反射型URL 参数点恶意链接?q=<script>alert(1)</script> 被回显到页面
存储型数据库(评论/昵称)所有访问者都中招评论区存了恶意脚本
DOM 型前端 JS 处理不当前端自己把数据当代码执行innerHTML = location.hash

1.2 防御(四条,按优先级) ​

① 输出转义(最根本)

js
// ❌ 危险:把用户输入当 HTML 解析
el.innerHTML = userInput
// ✅ 用 textContent(浏览器自动转义,不当 HTML 解析)
el.textContent = userInput
js
// 需要渲染 HTML 时,必须**净化**(用 DOMPurify)
import DOMPurify from 'dompurify'
el.innerHTML = DOMPurify.sanitize(userInput)

框架默认已转义:

  • React 的 {userInput} 默认转义(dangerouslySetInnerHTML 才不转义)
  • Vue 的 默认转义(v-html 才不转义)

🎯 99% 的 XSS 来源于:dangerouslySetInnerHTML / v-html / innerHTML / eval / new Function / document.write。 见到这些就要警觉。

② 不要用危险的 API

js
// ❌ 全部禁止
eval(code)
new Function(code)
setTimeout('alert(1)', 100)      // 字符串参数会被 eval
el.setAttribute('onclick', userInput)   // 事件属性注入
location.href = userInput        // javascript: 伪协议
// ❌ 危险 URL
<a href="javascript:alert(1)">   // 校验协议白名单:只允许 http/https/mailto

③ CSP(内容安全策略)——纵深防御

http
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-abc123';      # 只允许同源 + 带 nonce 的脚本
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  frame-ancestors 'none';                 # 防点击劫持
  object-src 'none';
  base-uri 'self';
html
<!-- 配合 nonce:每次请求生成随机值 -->
<script nonce="abc123">…</script>

💡 CSP 是第二道防线:即使发生了 XSS,也能阻止恶意脚本加载外部资源或外传数据。 建议先用 Content-Security-Policy-Report-Only 观察,再正式启用。

④ Cookie 加 HttpOnly

即使 XSS 成功,也偷不走 document.cookie 里的 HttpOnly Cookie(见第五节)。

1.3 富文本场景(编辑器/评论) ​

必须服务端 + 前端双净化,且用白名单而非黑名单:

js
DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
  ALLOWED_ATTR: ['href', 'title'],
  ALLOW_DATA_ATTR: false,
})

⚠️ 黑名单(过滤 <script>)一定会被绕过——有成千上万种变形(onerror=、svg、iframe…)。只能用白名单。


二、CSRF(跨站请求伪造) ​

原理:用户登录了 A 站,访问恶意 B 站,B 站自动发请求到 A 站, 浏览器自动带上 A 站的 Cookie → A 站以为是用户本人操作。

html
<!-- 恶意页面:img 标签会自动发 GET 请求 -->
<img src="https://bank.com/transfer?to=attacker&amount=10000">

2.1 防御(三种,可叠加) ​

① SameSite Cookie(首选,最省事)

http
Set-Cookie: session=xxx; HttpOnly; Secure; SameSite=Lax
值行为
Strict完全不随跨站请求发送(最安全,但影响从外链跳转过来的体验)
Lax(推荐)允许顶级导航 GET(点链接进来),阻止 POST 与子资源请求
None都发送,必须配合 Secure

② CSRF Token

服务端渲染页面时下发随机 token(放表单隐藏域或 meta)
→ 前端每次请求带上(Header 或 body)
→ 服务端校验
js
// 从 meta 读取,统一加到请求头
axios.defaults.headers.common['X-CSRF-Token'] =
  document.querySelector('meta[name="csrf-token"]').content

③ 自定义 Header + 校验 Origin/Referer

js
// 服务端校验
if (req.headers.origin !== 'https://your-site.com') return 403

🎯 现代实践:SameSite=Lax Cookie + 自定义请求头(如 X-Requested-With) 基本能挡住绝大多数 CSRF。用了 Authorization: Bearer 的方案(Token 不放 Cookie)天然免疫 CSRF。


三、点击劫持(Clickjacking) ​

原理:用透明 iframe 套住目标站点,诱导用户点击"看起来无害"的按钮。

防御:

http
X-Frame-Options: DENY                    # 或 SAMEORIGIN
Content-Security-Policy: frame-ancestors 'none'

X-Frame-Options 是老标准,CSP 的 frame-ancestors 是推荐做法,两者可同时用。


四、依赖供应链安全 ​

风险:npm 包被投毒(维护者账号被盗、恶意包名近似——typosquatting)。

防御:

措施做法
提交 lockfilepackage-lock.json / pnpm-lock.yaml 必须提交,CI 用 npm ci
定期审计npm audit / pnpm audit,或 Snyk、Dependabot
最少依赖每加一个依赖都是引入一份风险;小功能能自己写就自己写
检查包名确认拼写(如 lodash vs lodahs)、下载量、维护活跃度
锁定版本关键依赖用精确版本,避免 ^ 自动拉到恶意版本
忽略脚本.npmrc 里 ignore-scripts=true(阻断 postinstall 执行)
私有源企业用私有 registry + 安全扫描
bash
npm audit fix          # 自动修复(注意会改 lockfile,要 review)
pnpm audit --prod      # 只看生产依赖

⚠️ postinstall 脚本是重灾区:装包时自动执行的脚本拥有你机器的权限。


五、Token 存储:放哪 ​

方案XSS 风险CSRF 风险评价
localStorage❌ 高(JS 可读,XSS 直接偷走)✅ 无(不自动发送)⚠️ 常见但不推荐
sessionStorage❌ 同上✅ 无同上,关标签即失效
内存(变量)✅ 低(刷新即丢)✅ 无安全但体验差(刷新要重登)
HttpOnly Cookie✅ 低(JS 读不到)⚠️ 有(要配 SameSite + CSRF Token)✅ 推荐

推荐方案:

场景方案
常规 Web 应用HttpOnly + Secure + SameSite=Lax Cookie + CSRF Token
需要长期免登录HttpOnly Cookie 存 Refresh Token(长),Access Token 存内存(短,如 15 分钟)
纯 API/移动端Access Token 内存 + 安全存储(App 用 Keychain/Keystore)

最佳实践(无感刷新):

Access Token(15min)存内存  → 请求用
Refresh Token(7d)存 HttpOnly Cookie → 过期时换新 Access Token
js
// 响应拦截器:401 时刷新并重放
if (res.status === 401 && !isRefreshing) {
  const ok = await refresh()          // 用 HttpOnly Cookie 里的 refresh token
  return ok ? api.retry(originalRequest) : redirectToLogin()
}

❌ 不要把 Token 放 URL(会被记录在日志、Referer、浏览器历史里)。


六、传输与其他 ​

6.1 传输安全 ​

http
Strict-Transport-Security: max-age=31536000; includeSubDomains   # HSTS,强制 HTTPS
措施说明
全站 HTTPS含静态资源(否则会被"混合内容"拦截或降级)
HSTS强制浏览器只走 HTTPS,防 SSL Stripping
证书监控到期告警

6.2 其他常见风险 ​

风险防御
开放重定向?redirect= 必须校验白名单,否则被用于钓鱼
敏感信息进前端不要把密钥/内网地址打进 bundle(构建产物是公开的)
Source Map 泄漏用 hidden 且不部署到 CDN(见 A12)
iframe 嵌入第三方用 sandbox 属性限制权限
密码明文前端传输前至少做一次哈希(但仍需 HTTPS;真正安全靠后端加盐慢哈希)

自检清单 ​

  • [ ] 能说出 XSS 三种类型,以及"99% 的 XSS 来源"是哪些 API
  • [ ] 知道富文本必须用白名单净化(DOMPurify),黑名单会被绕过
  • [ ] 能说出 CSRF 的原理与三种防御,知道 SameSite=Lax 与 Bearer Token 方案
  • [ ] 能说出 Token 四种存储方案的 XSS/CSRF 风险对比,并选出推荐方案
  • [ ] 知道依赖供应链的防护措施(lockfile + 审计 + 最小依赖)

常见误区 ​

❌ 误区✅ 事实
"React/Vue 会自动转义,所以不会 XSS"用了 dangerouslySetInnerHTML/v-html 就没保护
"把 <script> 过滤掉就安全"黑名单必被绕过,只能白名单
"Token 存 localStorage 最方便"XSS 一来直接被偷走
"用了 HTTPS 就不用管 CSRF"HTTPS 只管传输,CSRF 是请求伪造,两回事
"前端做了校验就安全"前端校验只是体验,攻击者直接发请求;服务端必须校验
"环境变量里的密钥前端看不到"前端环境变量会被打进 bundle,是公开的

动手练习 ​

  1. 在一个页面里用 innerHTML 渲染用户输入,复现存储型 XSS,再用 textContent + DOMPurify 修复。
  2. 给项目配置 CSP(default-src 'self' + nonce),用 Report-Only 模式观察一天,看有哪些资源被拦截。
  3. 检查项目里 Token 存在哪里;如果是 localStorage,评估改成 "Access Token 内存 + Refresh Token HttpOnly Cookie" 的成本。

相关文档 ​