Das Kit schützt API-Endpunkte mit drei inneren Verteidigungsschichten: Rate Limiting (Upstash Redis mit kategoriebasierten Limits), Eingabevalidierung (zentrale Zod-Schemas) und Sanitization (XSS-, SQL-, Path-Traversal-, URL- und E-Mail-Schutz). Diese Schichten arbeiten zusammen, um sicherzustellen, dass auch eine Anfrage, die die Authentifizierung passiert, deine API nicht missbrauchen oder schädliche Daten einschleusen kann.
Für die übergeordnete Sicherheitsarchitektur siehe Sicherheitsübersicht. Für die Redis-Einrichtung siehe Caching & Redis.
Rate-Limit-Kategorien
Jeder API-Endpunkt gehört zu einer Kategorie mit unabhängigen benutzer- und IP-basierten Limits. So kannst du enge Limits für sensible Operationen (wie das Versenden von E-Mails) setzen, während allgemeine API-Endpunkte großzügiger konfiguriert bleiben:
src/lib/security/api-rate-limiter.ts — API_LIMITS Configuration
const API_LIMITS: Record<
APICategory,
{ user?: RateLimitConfig; ip?: RateLimitConfig }
> = {
upload: {
user: { requests: 10, window: '1 h', identifier: 'user' },
ip: { requests: 20, window: '1 h', identifier: 'ip' },
},
email: {
user: { requests: 5, window: '1 h', identifier: 'user' },
ip: { requests: 10, window: '1 h', identifier: 'ip' },
},
contact: {
ip: { requests: 3, window: '1 h', identifier: 'ip' },
},
payments: {
user: { requests: 20, window: '1 h', identifier: 'user' },
},
webhooks: {
ip: { requests: 100, window: '1 h', identifier: 'ip' },
},
api: {
user: { requests: 100, window: '1 h', identifier: 'user' },
ip: { requests: 200, window: '1 h', identifier: 'ip' },
},
}
Kategorie-Referenz
| Kategorie | Benutzer-Limit | IP-Limit | Typische Endpunkte |
|---|---|---|---|
upload | 10 Req/Stunde | 20 Req/Stunde | Datei-Upload (/api/upload) |
email | 5 Req/Stunde | 10 Req/Stunde | E-Mail-Versand (/api/email/send) |
contact | — | 3 Req/Stunde | Kontaktformular (/api/contact) — keine Auth erforderlich |
payments | 20 Req/Stunde | — | Checkout, Abonnementverwaltung |
webhooks | — | 100 Req/Stunde | Externe Webhook-Verarbeitung |
api | 100 Req/Stunde | 200 Req/Stunde | Allgemeiner API-Catch-All |
Wenn sowohl Benutzer- als auch IP-Limits für eine Kategorie konfiguriert sind, muss die Anfrage beide Prüfungen bestehen. Das restriktivste Ergebnis wird an den Client zurückgegeben.
withRateLimit verwenden
Die
withRateLimit-Middleware-Factory umschließt jeden API-Route-Handler mit automatischem Rate Limiting. Sie übernimmt die Identifikator-Extraktion, die Limit-Prüfung und die Antwort-Headers:src/lib/security/rate-limit-middleware.ts — withRateLimit Signature
export function withRateLimit(
category: APICategory,
handler: (request: NextRequest) => Promise<NextResponse>
): (request: NextRequest) => Promise<NextResponse> {
Grundlegende Verwendung
typescript
import { NextResponse } from 'next/server'
import { withRateLimit } from '@/lib/security/rate-limit-middleware'
export const POST = withRateLimit('upload', async (request) => {
// This code only runs if rate limit check passes
const formData = await request.formData()
// ... handle upload
return NextResponse.json({ success: true })
})
Die Middleware erledigt automatisch:
- Extraktion der Benutzer-ID aus der Clerk-Authentifizierung (oder Fallback auf Test-Benutzer)
- Extraktion der Client-IP aus
x-forwarded-for- oderx-real-ip-Headers - Prüfung der Benutzer- und IP-Rate-Limits für die gegebene Kategorie
- Rückgabe von 429 Too Many Requests mit Retry-Informationen bei Überschreitung
- Hinzufügen von
X-RateLimit-*-Headers zu jeder Antwort (Erfolg und Fehler) - Bei Middleware-Fehler: Fail-Open und direkter Aufruf des Handlers
Antwort-Headers
Jede Antwort von einem rate-limitierten Endpunkt enthält Standard-Headers, damit Clients ihre Nutzung programmatisch nachverfolgen können:
| Header | Beispielwert | Beschreibung |
|---|---|---|
X-RateLimit-Limit | 10 | Maximale Anfragen im aktuellen Zeitfenster |
X-RateLimit-Remaining | 7 | Verbleibende Anfragen bis zum Limit |
X-RateLimit-Reset | 1708012800000 | Unix-Timestamp (ms), wann das Fenster zurückgesetzt wird |
429-Antwortformat
Wenn das Rate Limit überschritten wird, erhält der Client eine strukturierte JSON-Antwort mit allen Informationen, die für die Implementierung einer Retry-Logik benötigt werden:
json
{
"error": "Rate limit exceeded",
"message": "Too many requests. Please try again in 45 minutes.",
"retryAfter": "45 minutes",
"limit": 10,
"remaining": 0,
"reset": 1708012800000
}
Das Feld
retryAfter liefert eine menschenlesbare Dauer. Das Feld reset liefert den genauen Unix-Timestamp für programmatisches Retry-Scheduling.Eingabevalidierung mit Zod
Jeder API-Endpunkt validiert seine Eingaben mithilfe zentraler Zod-Schemas, die in
apps/boilerplate/src/lib/validations/api-schemas.ts definiert sind. Das verhindert, dass fehlerhafte oder bösartige Daten deine Business-Logik erreichen.Schema-Organisation
Schemas sind nach Funktionsbereichen gruppiert:
| Schema | Felder | Verwendet von |
|---|---|---|
contactSchema | name (2–100 Zeichen), email, subject (3–200), message (10–5000) | /api/contact |
emailSchema | to (E-Mail), subject (1–200), html, text, type | /api/email/send |
createCheckoutSchema | variantId, productId, redirectUrl, cancelUrl, discountCode | /api/payments/checkout |
uploadFileSchema | filename (1–255), filesize (max 4,5 MB), filetype (MIME) | /api/upload |
newsletterSubscribeSchema | email, name (optional, 2–100) | /api/newsletter |
updateUserSchema | name, email, bio (max 500), avatar (URL) | /api/user/update |
bonusCreditCheckoutSchema | variantId (String, nicht leer) | /api/credits/checkout |
createCheckoutUrlSchema | variantId (String, nicht leer), embed (optionaler Boolean) | /api/checkout/create-url |
bonusPreferencesSchema | bonusCreditsAutoUse (Boolean) | /api/credits/preferences |
deleteUserSchema | confirmEmail (E-Mail) | /api/user/delete |
Repräsentatives Schema-Beispiel
src/lib/validations/api-schemas.ts — Contact Schema
export const contactSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters').max(100),
email: z.string().email('Invalid email address'),
subject: z.string().min(3, 'Subject must be at least 3 characters').max(200),
message: z
.string()
.min(10, 'Message must be at least 10 characters')
.max(5000),
})
Verwendungsmuster
typescript
import { contactSchema } from '@/lib/validations/api-schemas'
export const POST = withRateLimit('contact', async (request) => {
const body = await request.json()
// Validate input against schema
const result = contactSchema.safeParse(body)
if (!result.success) {
return NextResponse.json(
{ error: 'Validation failed', details: result.error.flatten() },
{ status: 400 }
)
}
// result.data is now fully typed and validated
const { name, email, subject, message } = result.data
// ... send contact email
})
Zod-Schemas dienen sowohl als Laufzeitvalidierung als auch als TypeScript-Typinferenz. Jedes Schema exportiert einen entsprechenden Typ (z. B.
ContactSchemaType), den du in deinem Anwendungscode verwenden kannst.Sanitization-Utilities
Nach der Validierung durchlaufen Daten Sanitization-Funktionen, die potenziell gefährliche Inhalte entfernen. Das Kit enthält eine umfassende Sanitization-Bibliothek:
Funktions-Referenz
| Funktion | Zweck | Beispieleingabe | Beispielausgabe |
|---|---|---|---|
sanitizeHtml() | XSS-Vektoren entfernen (Scripts, Iframes, Event-Handler) | <script>alert('xss')</script><p>Hi</p> | <p>Hi</p> |
sanitizeFilename() | Path-Traversal und Command-Injection verhindern | ../../../etc/passwd | etc_passwd |
sanitizeUrl() | Gefährliche Protokolle blockieren (javascript:, data:) | javascript:alert('xss') | "" (leer) |
sanitizeEmail() | E-Mail-Header-Injection verhindern | user@example.com\r\nBCC:hacker@evil.com | user@example.com |
escapeSqlLike() | LIKE-Wildcards für sichere SQL-Abfragen escapen | 100%_done | 100\%\_done |
sanitizeJson() | Prototype-Pollution-Keys entfernen | {"__proto__": {"admin": true}} | {} |
sanitizeText() | Steuerzeichen entfernen, Whitespace normalisieren | Hello\x00World | Hello World |
sanitizeInteger() | Numerische Eingaben auf sicheren Bereich begrenzen | "999999" (mit max=100) | 100 |
sanitizeBoolean() | Verschiedene Boolean-Darstellungen parsen | "yes" | true |
sanitizeErrorMessage() | Sensible Infos aus Fehlerantworten entfernen | Error: DB connection string: postgres://... | "An unexpected error occurred" |
Sanitization-Beispiel
typescript
import {
sanitizeFilename,
sanitizeUrl,
sanitizeHtml,
sanitizeText,
} from '@/lib/security/sanitization'
// Sanitize file upload metadata
const safeName = sanitizeFilename(userProvidedFilename) // Removes path traversal
const safeUrl = sanitizeUrl(userProvidedUrl) // Blocks javascript: URLs
const safeHtml = sanitizeHtml(userProvidedHtml) // Strips <script> tags
const safeText = sanitizeText(userProvidedBio, 500) // Removes control chars, limits length
Validierung (Zod) lehnt fehlerhafte Eingaben vollständig ab — die Anfrage schlägt mit 400 fehl. Sanitization transformiert Eingaben, um sie sicher zu machen — die Anfrage wird mit bereinigten Daten fortgesetzt. Verwende Validierung für strukturelle Anforderungen (Feldtypen, Längen) und Sanitization für die Inhaltsbereinigung (Scripts aus HTML entfernen, Dateinamen normalisieren).
Vollständiges API-Routen-Beispiel
Hier ist eine vollständige API-Route, die alle drei inneren Verteidigungsschichten gemeinsam demonstriert — Rate Limiting, Validierung und Sanitization:
typescript
import { NextRequest, NextResponse } from 'next/server'
import { withRateLimit } from '@/lib/security/rate-limit-middleware'
import { contactSchema } from '@/lib/validations/api-schemas'
import { sanitizeText, sanitizeEmail } from '@/lib/security/sanitization'
export const POST = withRateLimit('contact', async (request: NextRequest) => {
// Layer 2: Rate limiting (handled by withRateLimit wrapper)
// Parse request body
const body = await request.json()
// Layer 3: Input validation with Zod
const result = contactSchema.safeParse(body)
if (!result.success) {
return NextResponse.json(
{ error: 'Validation failed', details: result.error.flatten() },
{ status: 400 }
)
}
// Layer 4: Sanitization
const name = sanitizeText(result.data.name, 100)
const email = sanitizeEmail(result.data.email)
const subject = sanitizeText(result.data.subject, 200)
const message = sanitizeText(result.data.message, 5000)
if (!email) {
return NextResponse.json(
{ error: 'Invalid email address' },
{ status: 400 }
)
}
// Business logic — all input is now validated and sanitized
await sendContactEmail({ name, email, subject, message })
return NextResponse.json({ success: true })
})
Dieses Muster stellt sicher:
- Rate Limiting stoppt Missbrauch, bevor jegliche Verarbeitung stattfindet
- Zod-Validierung lehnt strukturell ungültige Anfragen mit klaren Fehlermeldungen ab
- Sanitization bereinigt die Daten, um XSS, Injection und andere Angriffe zu verhindern
- Business-Logik erhält ausschließlich sichere, validierte und typisierte Daten
Rate Limits anpassen
Eine neue Kategorie hinzufügen
Um eine benutzerdefinierte Rate-Limit-Kategorie hinzuzufügen, erweitere die
API_LIMITS-Konfiguration und den APICategory-Typ:typescript
// In apps/boilerplate/src/lib/security/api-rate-limiter.ts
// 1. Add to the union type
export type APICategory =
| 'upload'
| 'email'
| 'contact'
| 'payments'
| 'webhooks'
| 'api'
| 'search' // New category
// 2. Add to API_LIMITS
const API_LIMITS = {
// ... existing categories
search: {
user: { requests: 50, window: '1 h', identifier: 'user' },
ip: { requests: 100, window: '1 h', identifier: 'ip' },
},
}
Dann in deiner API-Route verwenden:
typescript
export const GET = withRateLimit('search', async (request) => {
// Search logic
})
Bestehende Limits anpassen
Ändere die Werte für
requests und window im API_LIMITS-Objekt. Der Sliding-Window-Algorithmus verarbeitet die neuen Werte automatisch — keine Redis-Neukonfiguration erforderlich.Auch wenn das System ohne Redis funktioniert (Fail-Open), sollte Redis in der Produktion immer konfiguriert sein. Ohne Redis gibt es keinen Schutz gegen API-Missbrauch. Überwache die Warnmeldung
"Rate limiting disabled", um Fehlkonfigurationen frühzeitig zu erkennen.Wichtige Dateien
| Datei | Zweck |
|---|---|
apps/boilerplate/src/lib/security/api-rate-limiter.ts | Kategoriebasiertes Rate Limiting mit Upstash Redis |
apps/boilerplate/src/lib/security/rate-limit-middleware.ts | withRateLimit()-Factory und checkRateLimitOnly()-Helper |
apps/boilerplate/src/lib/validations/api-schemas.ts | Zentrale Zod-Schemas für alle API-Endpunkte |
apps/boilerplate/src/lib/security/sanitization.ts | Sanitization-Funktionen (HTML, Dateiname, URL, E-Mail, SQL, JSON) |