Rate Limiting & Validierung

Kategoriebasiertes API-Rate-Limiting mit Upstash Redis, Zod-Eingabevalidierung und XSS-Sanitization-Utilities

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

KategorieBenutzer-LimitIP-LimitTypische Endpunkte
upload10 Req/Stunde20 Req/StundeDatei-Upload (/api/upload)
email5 Req/Stunde10 Req/StundeE-Mail-Versand (/api/email/send)
contact3 Req/StundeKontaktformular (/api/contact) — keine Auth erforderlich
payments20 Req/StundeCheckout, Abonnementverwaltung
webhooks100 Req/StundeExterne Webhook-Verarbeitung
api100 Req/Stunde200 Req/StundeAllgemeiner 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:
  1. Extraktion der Benutzer-ID aus der Clerk-Authentifizierung (oder Fallback auf Test-Benutzer)
  2. Extraktion der Client-IP aus x-forwarded-for- oder x-real-ip-Headers
  3. Prüfung der Benutzer- und IP-Rate-Limits für die gegebene Kategorie
  4. Rückgabe von 429 Too Many Requests mit Retry-Informationen bei Überschreitung
  5. Hinzufügen von X-RateLimit-*-Headers zu jeder Antwort (Erfolg und Fehler)
  6. 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:
HeaderBeispielwertBeschreibung
X-RateLimit-Limit10Maximale Anfragen im aktuellen Zeitfenster
X-RateLimit-Remaining7Verbleibende Anfragen bis zum Limit
X-RateLimit-Reset1708012800000Unix-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:
SchemaFelderVerwendet von
contactSchemaname (2–100 Zeichen), email, subject (3–200), message (10–5000)/api/contact
emailSchemato (E-Mail), subject (1–200), html, text, type/api/email/send
createCheckoutSchemavariantId, productId, redirectUrl, cancelUrl, discountCode/api/payments/checkout
uploadFileSchemafilename (1–255), filesize (max 4,5 MB), filetype (MIME)/api/upload
newsletterSubscribeSchemaemail, name (optional, 2–100)/api/newsletter
updateUserSchemaname, email, bio (max 500), avatar (URL)/api/user/update
bonusCreditCheckoutSchemavariantId (String, nicht leer)/api/credits/checkout
createCheckoutUrlSchemavariantId (String, nicht leer), embed (optionaler Boolean)/api/checkout/create-url
bonusPreferencesSchemabonusCreditsAutoUse (Boolean)/api/credits/preferences
deleteUserSchemaconfirmEmail (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

FunktionZweckBeispieleingabeBeispielausgabe
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/passwdetc_passwd
sanitizeUrl()Gefährliche Protokolle blockieren (javascript:, data:)javascript:alert('xss')"" (leer)
sanitizeEmail()E-Mail-Header-Injection verhindernuser@example.com\r\nBCC:hacker@evil.comuser@example.com
escapeSqlLike()LIKE-Wildcards für sichere SQL-Abfragen escapen100%_done100\%\_done
sanitizeJson()Prototype-Pollution-Keys entfernen{"__proto__": {"admin": true}}{}
sanitizeText()Steuerzeichen entfernen, Whitespace normalisierenHello\x00WorldHello 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 entfernenError: 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

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:
  1. Rate Limiting stoppt Missbrauch, bevor jegliche Verarbeitung stattfindet
  2. Zod-Validierung lehnt strukturell ungültige Anfragen mit klaren Fehlermeldungen ab
  3. Sanitization bereinigt die Daten, um XSS, Injection und andere Angriffe zu verhindern
  4. 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.

Wichtige Dateien

DateiZweck
apps/boilerplate/src/lib/security/api-rate-limiter.tsKategoriebasiertes Rate Limiting mit Upstash Redis
apps/boilerplate/src/lib/security/rate-limit-middleware.tswithRateLimit()-Factory und checkRateLimitOnly()-Helper
apps/boilerplate/src/lib/validations/api-schemas.tsZentrale Zod-Schemas für alle API-Endpunkte
apps/boilerplate/src/lib/security/sanitization.tsSanitization-Funktionen (HTML, Dateiname, URL, E-Mail, SQL, JSON)