外观
前端安全:XSS、CSRF 与 CSP 实战
前端安全最危险的地方在于:漏洞往往由一行看似正常的代码引入,而影响可能波及所有用户。
这篇文章不罗列 OWASP 条目,而是围绕前端最常见的三类风险——XSS、CSRF 和供应链攻击——讲清楚攻击路径、防御手段和落地配置。
XSS:把不可信数据当成代码
XSS 的本质是用户可控的数据被当作 HTML 或 JavaScript 执行。常见类型:
- 存储型:恶意内容存入数据库,所有访问者都会触发。评论区、昵称、富文本是重灾区。
- 反射型:恶意参数通过 URL 反射到页面,通常配合钓鱼链接。
- DOM 型:前端代码直接把 URL、
localStorage等数据写入危险 DOM API。
识别危险汇点
ts
// 危险:直接写入 HTML
element.innerHTML = comment.content
// 危险:插入 HTML 片段
element.insertAdjacentHTML('beforeend', userContent)
// 危险:把用户输入当作链接
link.href = userInput
// 危险:动态执行字符串
eval(userInput)框架的自动转义能挡住大部分模板插值,但不会保护这些"逃生舱":
- Vue 的
v-html - React 的
dangerouslySetInnerHTML - 直接操作 DOM 的
innerHTML、outerHTML、document.write
原则是:能不渲染 HTML 就不渲染。确需富文本时,必须经过严格的白名单清洗:
ts
import DOMPurify from 'dompurify'
const cleanHtml = DOMPurify.sanitize(rawHtml, {
USE_PROFILES: { html: true },
FORBID_TAGS: ['style', 'iframe', 'form'],
FORBID_ATTR: ['onerror', 'onload']
})
container.innerHTML = cleanHtmlDOMPurify 不能替代业务校验。允许用户上传 HTML 的场景,仍要限制标签、属性和 URL 协议。
URL 协议白名单
javascript: 协议可以直接执行代码,所有跳转和图片地址都要校验:
ts
function toSafeUrl(input: string) {
const url = new URL(input, window.location.origin)
const allowed = ['http:', 'https:', 'mailto:']
if (!allowed.includes(url.protocol)) {
throw new Error('Unsupported protocol')
}
return url.href
}CSP:限制代码来源
内容安全策略(CSP)通过响应头告诉浏览器,哪些资源可以加载和执行。它是 XSS 的最后一道防线:
http
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-RANDOM' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
upgrade-insecure-requests几个关键点:
script-src使用 nonce 或 hash,避免'unsafe-inline'。'strict-dynamic'让受信任脚本动态加载的脚本也被允许。object-src 'none'禁用 Flash 等插件,减少攻击面。frame-ancestors 'none'防止页面被嵌入,配合X-Frame-Options防点击劫持。connect-src限制接口域名,攻击者即使注入脚本也难以外传数据。- 上线前先用
Content-Security-Policy-Report-Only收集违规报告,避免直接打断业务。
带 nonce 的服务端实现需要每次请求生成随机值,并注入到脚本标签:
ts
import crypto from 'node:crypto'
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('base64')
res.setHeader(
'Content-Security-Policy',
[
"default-src 'self'",
`script-src 'self' 'nonce-${res.locals.cspNonce}' 'strict-dynamic'`,
"style-src 'self' 'unsafe-inline'",
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'"
].join('; ')
)
next()
})如果项目使用 Trusted Types,可以进一步禁止危险的字符串赋值:
ts
const policy = window.trustedTypes?.createPolicy('default', {
createHTML: (input: string) => DOMPurify.sanitize(input)
})配合 CSP 的 require-trusted-types-for 'script',innerHTML 参数必须是 TrustedHTML 类型,能系统性减少 DOM XSS。
CSRF:借用用户的登录态
CSRF(跨站请求伪造)利用浏览器会自动携带 Cookie 的特性,诱导已登录用户在不知情的情况下发起请求。
防御分三层:
1. Cookie 属性
ts
res.cookie('session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/'
})HttpOnly阻止 JavaScript 读取,降低 XSS 窃取风险。Secure只允许 HTTPS 传输。SameSite=Lax阻止大多数跨站 POST 请求携带 Cookie。Strict更严格,但会影响从外部链接进入时的登录态。
2. CSRF Token
对状态变更请求(POST、PUT、DELETE)校验一次性令牌,令牌由服务端生成并绑定会话:
ts
app.post('/api/orders', (req, res) => {
const token = req.get('x-csrf-token')
if (!token || token !== req.session.csrfToken) {
return res.status(403).json({ message: 'CSRF token invalid' })
}
// 处理业务
})前端从页面注入或专用接口获取 token,放在请求头中。攻击者无法读取跨域页面的 token,也就无法构造有效请求。
3. 校验 Origin / Referer
对来自浏览器的请求,校验 Origin 是否在白名单内。它不能替代 token,但能作为额外防线:
ts
const allowedOrigins = new Set(['https://app.example.com'])
app.use((req, res, next) => {
const origin = req.get('origin')
if (origin && !allowedOrigins.has(origin)) {
return res.status(403).json({ message: 'Origin not allowed' })
}
next()
})注意:CORS 不能防 CSRF。CORS 限制的是"读取响应",而 CSRF 只要求请求被执行。
Token 应该存在哪里
这是前端安全中最常争论的问题:
| 存储位置 | XSS 风险 | CSRF 风险 | 适用场景 |
|---|---|---|---|
localStorage | 高,脚本可直接读取 | 低,不会自动携带 | 纯前端 API,接受风险 |
HttpOnly Cookie | 低,脚本不可读 | 高,需 CSRF 防护 | 同域 Web 应用 |
| 内存变量 | 中,刷新丢失 | 低 | 短期 access token |
推荐组合:短期 access token 放内存,refresh token 放 HttpOnly + Secure + SameSite Cookie。刷新接口同时校验 CSRF token 和 Origin。这样既不把长期凭证暴露给脚本,也能控制泄露窗口。
供应链与第三方脚本
前端安全不只来自自己的代码:
- 锁定依赖版本,提交 lockfile,CI 使用
--frozen-lockfile。 - 定期执行
npm audit,但对告警做人工评估,不要盲目自动升级。 - 对 CDN 引入的脚本使用 Subresource Integrity(SRI):
html
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-BASE64_HASH"
crossorigin="anonymous"
></script>- 第三方脚本(统计、客服、广告)通过 CSP 限制来源,并评估其权限。
- 避免在页面中直接加载来源不明的微前端远程模块。
安全响应头清单
http
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy: default-src 'self'; object-src 'none'; frame-ancestors 'none'nosniff 阻止浏览器猜测 MIME 类型,降低上传文件被当作脚本执行的风险。Referrer-Policy 防止 URL 中的敏感参数通过 Referer 泄漏。
常见坑
- 只做前端校验。前端校验是体验,服务端校验才是安全边界。
- CSP 直接上生产。先 Report-Only 收集违规,再逐步收紧,否则容易白屏。
- 认为用了框架就安全。
v-html和dangerouslySetInnerHTML仍是高危入口。 - 把 token 放 localStorage 还忽略 XSS。一旦有 XSS,token 可直接被读取。
- 第三方脚本无限制。任意第三方脚本都拥有页面同源权限,等于把安全交给了对方。
- 忽略依赖漏洞。构建工具、UI 库、polyfill 都可能成为攻击入口。
小结
前端安全是纵深防御:输入层做校验与清洗,输出层做编码,运行时用 CSP 限制能力,会话层用 Cookie 属性和 CSRF token 防护,供应链层锁定依赖。任何单点措施都不够,组合起来才能把风险降到可接受范围。