fix: apply CSRF protection and centralize rate limiting

- Migrate /api/posts from legacy checkRateLimit to central searchLimiter
- Add IP blocklist check and rateLimitHeaders to /api/posts
- Apply CSRF validation to /api/send-email (POST)
- Apply CSRF validation to /api/users/login (POST)
- Apply CSRF validation to /api/auth/login (POST)

CSRF protection uses the Double Submit Cookie pattern which:
- Skips safe methods (GET, HEAD, OPTIONS)
- Allows server-to-server requests with Authorization header
- Validates Origin header for browser requests
- Requires matching tokens in header and cookie for browser POSTs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Martin Porwoll 2025-12-07 23:47:07 +00:00
parent cb2e903db5
commit 2fae62eaf3
4 changed files with 51 additions and 11 deletions

View file

@ -4,32 +4,41 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload' import { getPayload } from 'payload'
import config from '@payload-config' import config from '@payload-config'
import { getPostsByCategory, checkRateLimit } from '@/lib/search' import { getPostsByCategory } from '@/lib/search'
import type { Category } from '@/payload-types' import type { Category } from '@/payload-types'
import {
searchLimiter,
rateLimitHeaders,
getClientIpFromRequest,
isIpBlocked,
} from '@/lib/security'
// Validation constants // Validation constants
const MAX_LIMIT = 50 const MAX_LIMIT = 50
const DEFAULT_LIMIT = 10 const DEFAULT_LIMIT = 10
const POSTS_RATE_LIMIT = 30
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
// Rate limiting // IP-Blocklist prüfen
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || const ip = getClientIpFromRequest(request)
request.headers.get('x-real-ip') || if (isIpBlocked(ip)) {
'unknown' return NextResponse.json(
{ error: 'Access denied' },
{ status: 403 },
)
}
const rateLimit = checkRateLimit(ip) // Rate limiting (zentral)
const rateLimit = await searchLimiter.check(ip)
if (!rateLimit.allowed) { if (!rateLimit.allowed) {
return NextResponse.json( return NextResponse.json(
{ error: 'Too many requests. Please try again later.' }, { error: 'Too many requests. Please try again later.' },
{ {
status: 429, status: 429,
headers: { headers: rateLimitHeaders(rateLimit, POSTS_RATE_LIMIT),
'Retry-After': String(rateLimit.retryAfter || 60),
'X-RateLimit-Remaining': '0',
}, },
}
) )
} }

View file

@ -26,6 +26,7 @@ import {
rateLimitHeaders, rateLimitHeaders,
getClientIpFromRequest, getClientIpFromRequest,
isIpBlocked, isIpBlocked,
validateCsrf,
} from '@/lib/security' } from '@/lib/security'
/** /**
@ -53,6 +54,15 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
) )
} }
// CSRF-Schutz für Browser-basierte Requests
const csrfResult = validateCsrf(req)
if (!csrfResult.valid) {
return NextResponse.json(
{ success: false, error: 'CSRF validation failed' },
{ status: 403 },
)
}
// Rate-Limiting prüfen (Anti-Brute-Force) // Rate-Limiting prüfen (Anti-Brute-Force)
const rateLimit = await authLimiter.check(clientIp) const rateLimit = await authLimiter.check(clientIp)
if (!rateLimit.allowed) { if (!rateLimit.allowed) {

View file

@ -8,6 +8,7 @@ import {
rateLimitHeaders, rateLimitHeaders,
validateIpAccess, validateIpAccess,
createSafeLogger, createSafeLogger,
validateCsrf,
} from '@/lib/security' } from '@/lib/security'
const RATE_LIMIT_MAX = 10 const RATE_LIMIT_MAX = 10
@ -74,6 +75,16 @@ export async function POST(req: NextRequest) {
) )
} }
// CSRF-Schutz für Browser-basierte Requests
const csrfResult = validateCsrf(req)
if (!csrfResult.valid) {
logger.warn('CSRF validation failed', { reason: csrfResult.reason })
return NextResponse.json(
{ error: 'CSRF validation failed' },
{ status: 403 },
)
}
const payload = await getPayload({ config }) const payload = await getPayload({ config })
// Authentifizierung prüfen (aus Cookie/Header) // Authentifizierung prüfen (aus Cookie/Header)

View file

@ -24,6 +24,7 @@ import {
rateLimitHeaders, rateLimitHeaders,
getClientIpFromRequest, getClientIpFromRequest,
isIpBlocked, isIpBlocked,
validateCsrf,
} from '@/lib/security' } from '@/lib/security'
/** /**
@ -50,6 +51,15 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
) )
} }
// CSRF-Schutz für Browser-basierte Requests
const csrfResult = validateCsrf(req)
if (!csrfResult.valid) {
return NextResponse.json(
{ errors: [{ message: 'CSRF validation failed' }] },
{ status: 403 },
)
}
// Rate-Limiting prüfen (Anti-Brute-Force) // Rate-Limiting prüfen (Anti-Brute-Force)
const rateLimit = await authLimiter.check(clientIp) const rateLimit = await authLimiter.check(clientIp)
if (!rateLimit.allowed) { if (!rateLimit.allowed) {