19.03-CSRF防护
要点
- CSRF(Cross-Site Request Forgery):攻击者诱导已登录用户在不知情的情况下向目标网站发起请求
- CSRF 利用的是浏览器自动携带 Cookie 的机制——用户登录了 A 站,访问 B 站时浏览器会自动带上 A 站的 Cookie
- 三类请求不受 CSRF 影响:纯 GET 请求(不应有副作用)、非 Cookie 认证(Bearer Token 在 Authorization 头里,B 站读不到)、SameSite Cookie
- 防御方案:CSRF Token、SameSite Cookie、自定义 Header 验证——三选一或叠加
- Hono 的
hono/csrf中间件封装了 CSRF Token 的生成和校验 - 前后端分离 + JWT 的架构天然免疫传统 CSRF——但如果不注意 Cookie 配置,仍有风险
1. CSRF 攻击原理
CSRF 攻击的核心:浏览器会自动携带目标站点的 Cookie 发起请求。
攻击过程:
- 用户在银行网站
bank.com登录,浏览器保存了bank.com的 session Cookie - 用户访问恶意网站
evil.com evil.com的页面里藏了一个表单,自动提交到bank.com/transfer- 浏览器收到这个请求,会自动带上用户在
bank.com的 Cookie bank.com看到合法的 Cookie + 转账请求,以为是用户自己操作的- 转账完成,用户损失资金
1.1 攻击示例
<!-- evil.com 的页面 -->
<body onload="document.getElementById('csrf').submit()">
<form id="csrf" action="https://bank.com/api/transfer" method="POST">
<input name="to" value="attacker-account">
<input name="amount" value="10000">
</form>
</body>用户打开这个页面,页面自动提交表单到银行。浏览器自动带上银行 Cookie,银行服务器以为是用户操作,执行转账。
1.2 更隐蔽的方式
除了表单,还可以用 <img> 标签发 GET 请求:
<!-- 用 GET 请求修改密码(如果服务端错误地允许 GET 修改) -->
<img src="https://bank.com/api/change-password?new=hacked123" style="display:none">或者用 fetch() / XMLHttpRequest 发 POST(如果服务端 CORS 配置宽松):
<script>
fetch('https://bank.com/api/transfer', {
method: 'POST',
credentials: 'include', // 带 Cookie
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ to: 'attacker', amount: 10000 })
})
</script>2. 什么情况下会被 CSRF 攻击
CSRF 攻击成立需要满足三个条件:
- 用户已登录目标站点(浏览器有目标站点的 Cookie)
- 请求会自动携带 Cookie(浏览器默认行为)
- 请求能产生副作用(转账、修改密码、删除数据等)
缺任何一个条件都不成立。
2.1 不受 CSRF 影响的场景
纯 API Token 认证:如果你用 Authorization: Bearer xxx 传 token,攻击者的 evil.com 页面拿不到这个 token(跨域读不到),所以伪造的请求不带 token,服务端直接拒绝。
// 这种认证方式天然免疫传统 CSRF
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
// evil.com 的 fetch 拿不到你的 token,这个 header 是空的
if (!token) return c.json({ error: 'Unauthorized' }, 401)
// ...
})SameSite Cookie:如果 Cookie 设置了 SameSite=Strict 或 SameSite=Lax,跨站请求不会携带 Cookie。后面详细讲。
纯 GET 接口:GET 请求按 HTTP 语义不应该有副作用。虽然攻击者能发起 GET 请求,但服务端不应该在 GET 里做修改操作。
2.2 容易被 CSRF 攻击的场景
Cookie 认证 + 跨站表单/请求:
// 危险:用 Cookie 做认证 + 没有 CSRF 防护
app.post('/api/transfer', async (c) => {
const session = c.req.header('Cookie') // ← 跨站请求也会带 Cookie
const user = await getSession(session)
if (!user) return c.json({ error: 'Unauthorized' }, 401)
// 攻击者的请求也能走到这里
const { to, amount } = await c.req.json()
await transfer(user.id, to, amount)
return c.json({ ok: true })
})3. 防御方案一:CSRF Token
最经典的 CSRF 防御。原理:服务端给每个会话生成一个随机 token,每次写操作必须携带这个 token。攻击者拿不到 token,就无法伪造合法请求。
3.1 工作流程
1. 用户访问表单页面 → 服务端生成 CSRF token,嵌在表单里
2. 用户提交表单 → 表单携带 CSRF token
3. 服务端校验 token → 匹配则放行,不匹配则拒绝3.2 Hono 的 CSRF 中间件
// src/middleware/csrf.ts
import { Hono } from 'hono'
import { csrf } from 'hono/csrf'
const app = new Hono()
// 最简单的用法:校验所有 POST/PUT/DELETE 请求的 CSRF token
app.use('*', csrf({
secret: process.env.CSRF_SECRET, // 用于加密 token 的密钥
}))hono/csrf 中间件做了两件事:
- 在响应里设置 CSRF token(通过 Cookie 或响应体)
- 校验写操作请求里携带的 CSRF token
3.3 手动实现 CSRF Token
理解原理很重要:
// src/middleware/csrf-manual.ts
import { Context, Next } from 'hono'
// 生成 CSRF token
async function generateCSRFToken(env: { CSRF_KV: KVNamespace }, sessionId: string) {
const token = crypto.randomUUID()
await env.CSRF_KV.put(`csrf:${sessionId}`, token, {
expirationTtl: 86400,
})
return token
}
// 校验 CSRF token
async function verifyCSRFToken(env: { CSRF_KV: KVNamespace }, sessionId: string, token: string) {
const stored = await env.CSRF_KV.get(`csrf:${sessionId}`)
return stored === token
}
// 中间件
export function csrfProtection() {
return async (c: Context, next: Next) => {
// GET 请求:生成 token,放到 Cookie 里
if (c.req.method === 'GET') {
const token = await generateCSRFToken(c.env, c.get('sessionId'))
c.header('Set-Cookie', `csrf_token=${token}; Path=/; SameSite=Strict; Secure`)
await next()
return
}
// 写操作:校验 token
const token = c.req.header('X-CSRF-Token')
|| (await c.req.parseBody())['csrf_token']
if (!token || !await verifyCSRFToken(c.env, c.get('sessionId'), token)) {
return c.json({ error: 'Invalid CSRF token' }, 403)
}
await next()
}
}3.4 前端如何携带 CSRF Token
表单提交:
<form method="POST" action="/api/transfer">
<input type="hidden" name="csrf_token" value="xxx">
<!-- 表单其他字段 -->
</form>AJAX 请求:
// 从 Cookie 或 meta 标签读取 token
const token = document.querySelector('meta[name="csrf-token"]').content
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': token, // 放在自定义 Header 里
'Content-Type': 'application/json',
},
body: JSON.stringify({ to: 'alice', amount: 100 }),
})4. 防御方案二:SameSite Cookie
Cookie 的 SameSite 属性控制浏览器在跨站请求时是否携带 Cookie:
| 值 | 行为 |
|---|---|
Strict | 任何跨站请求都不携带 Cookie |
Lax | 顶级导航(用户点击链接)携带,其他跨站请求(表单、fetch、img)不携带 |
None | 跨站请求也携带(必须同时设置 Secure) |
// 设置 SameSite Cookie
c.header('Set-Cookie', 'session=xxx; Path=/; SameSite=Lax; Secure; HttpOnly')4.1 SameSite=Lax 的默认行为
现代浏览器(Chrome 80+)默认 Cookie 的 SameSite 值为 Lax。这意味着:
- 用户点击
evil.com上的链接跳转到bank.com→ 携带 Cookie(这是正常的导航行为) evil.com页面里的表单自动提交到bank.com→ 不携带 Cookie(CSRF 被挡住)evil.com里的fetch()请求到bank.com→ 不携带 Cookie(CSRF 被挡住)evil.com里的<img src="bank.com/...">→ 不携带 Cookie(CSRF 被挡住)
SameSite=Lax 已经挡住了大部分 CSRF 攻击。
4.2 SameSite=None 的危险
如果你的 Cookie 设置了 SameSite=None; Secure(跨站需要携带 Cookie 的场景,比如第三方登录),那就完全失去了 SameSite 的 CSRF 保护——必须叠加其他防御方案。
// 危险:SameSite=None + 没有 CSRF Token
c.header('Set-Cookie', 'session=xxx; SameSite=None; Secure')
// ← 任何跨站请求都会带 Cookie,CSRF 攻击可以成功5. 防御方案三:自定义 Header 验证
CSRF 攻击无法通过跨站 fetch() 发送带有自定义 Header 的请求——因为 Content-Type: application/json 和自定义 Header 都会触发 CORS 预检,而攻击者的站点不会通过预检。
如果你的 API 要求请求带一个自定义 Header(比如 X-Requested-With: XMLHttpRequest),跨站的表单和 <img> 请求都做不到:
// 服务端:要求自定义 Header
app.post('/api/transfer', async (c, next) => {
if (c.req.header('X-Requested-With') !== 'XMLHttpRequest') {
return c.json({ error: 'Missing required header' }, 400)
}
await next()
})// 前端:fetch 带上自定义 Header
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json',
},
body: JSON.stringify({ to: 'alice', amount: 100 }),
})CSRF 攻击的表单和 <img> 无法设置自定义 Header,所以请求会被拒绝。
6. JWT + 前后端分离还需要 CSRF 防护吗?
大多数情况下不需要,但有例外。
6.1 不需要 CSRF 的情况
JWT 存在 localStorage 里,前端每次请求手动放到 Authorization 头里:
// JWT 存在 localStorage,手动加到 Header
const token = localStorage.getItem('jwt')
fetch('/api/transfer', {
headers: { 'Authorization': `Bearer ${token}` },
})攻击者的 evil.com 读不到你 localStorage 里的 JWT,所以伪造的请求没有 Authorization 头,服务端直接拒绝。
6.2 仍然需要 CSRF 的情况
JWT 存在 HttpOnly Cookie 里(这是更安全的做法——防止 XSS 偷 token):
// JWT 存在 HttpOnly Cookie
c.header('Set-Cookie', `jwt=${token}; Path=/; HttpOnly; Secure; SameSite=Lax`)此时浏览器跨站请求会自动带上 JWT Cookie——CSRF 攻击又可以成功了。
解决方案:
- SameSite=Lax(默认值)→ 大部分 CSRF 被挡住
- 叠加 CSRF Token → 双重保险
- 自定义 Header 验证 → 配合 JWT Cookie 使用
// JWT Cookie + 自定义 Header 双重验证
app.post('/api/transfer', async (c) => {
// 1. 自定义 Header(挡住 CSRF)
if (c.req.header('X-Requested-With') !== 'XMLHttpRequest') {
return c.json({ error: 'Bad request' }, 400)
}
// 2. JWT Cookie(认证)
const jwt = c.req.header('Cookie')?.match(/jwt=([^;]+)/)?.[1]
if (!jwt) return c.json({ error: 'Unauthorized' }, 401)
const user = await verifyJWT(jwt, c.env.JWT_SECRET)
if (!user) return c.json({ error: 'Invalid token' }, 401)
// 3. 业务逻辑
const { to, amount } = await c.req.json()
await transfer(user.id, to, amount)
return c.json({ ok: true })
})7. AI 场景的 CSRF
AI 接口(比如 /api/ai/chat)通常是 POST 请求,用 JWT 或 API Key 认证。CSRF 风险取决于认证方式:
| 认证方式 | CSRF 风险 | 是否需要额外防护 |
|---|---|---|
| Bearer Token in Header | 低 | 不需要 |
| JWT in HttpOnly Cookie | 中 | SameSite=Lax + 自定义 Header |
| Session Cookie | 高 | CSRF Token 或 SameSite |
| API Key in Header | 低 | 不需要 |
AI 接口如果被 CSRF 攻击,后果可能很严重——攻击者可以让用户的 AI 做不期望的操作(生成内容、消耗配额、调用工具)。即使是 POST + Bearer Token 架构,也建议在关键操作(调用工具、删除对话)加 CSRF Token 做双重保险。
8. CSRF 防护方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| CSRF Token | 经典方案,安全性高 | 需要前后端配合,实现稍复杂 | Cookie 认证的 Web 应用 |
| SameSite Cookie | 零代码(现代浏览器默认 Lax) | 旧浏览器不支持,SameSite=None 场景无效 | 大部分现代 Web 应用 |
| 自定义 Header | 简单,不需要 token 管理 | 只能用于 AJAX,不能用于表单提交 | 前后端分离的 API |
| Referer/Origin 检查 | 简单 | 隐私设置可能去掉这些 Header | 辅助验证,不建议作为唯一方案 |
建议:SameSite=Lax(默认)+ 自定义 Header 覆盖大部分场景。Cookie 认证 + 关键操作叠加 CSRF Token。
9. 小结
CSRF 利用的是浏览器自动携带 Cookie 的机制——攻击者诱导已登录用户发起跨站请求,浏览器自动带上目标站点的 Cookie。
三类防御方案:
- CSRF Token:服务端生成随机 token,每次写操作必须携带
- SameSite Cookie:
Lax挡住大部分跨站请求(现代浏览器默认) - 自定义 Header:跨站表单和
<img>无法设置自定义 Header
前后端分离 + JWT 存在 localStorage 的架构天然免疫传统 CSRF。JWT 存在 HttpOnly Cookie 里时需要叠加防御。
AI 接口的 CSRF 防护不能忽视——特别是调用工具、消耗配额的接口。
下一篇讲 XSS 防护——CSRF 管的是「跨站请求伪造」,XSS 管的是「跨站脚本注入」。