Das Kit wendet 10 Security-Headers und routenbewusste CORS-Richtlinien über die Middleware-Chain auf jede Antwort an. Diese bilden die äußerste Verteidigungsschicht — sie schützen gegen Clickjacking, MIME-Sniffing, Protokoll-Downgrade-Angriffe und unberechtigte Cross-Origin-Anfragen, bevor dein Anwendungscode überhaupt ausgeführt wird.
Für die übergeordnete Sicherheitsarchitektur siehe Sicherheitsübersicht. Für Rate Limiting und Eingabevalidierung siehe Rate Limiting & Validierung.
Security-Headers
Jede Antwort enthält einen umfassenden Satz an Security-Headers. Die Standardwerte folgen Best Practices der Branche und den OWASP-Empfehlungen:
src/lib/security/security-headers.ts — Default Configuration
const DEFAULT_CONFIG: Required<SecurityHeadersConfig> = {
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true,
},
xFrameOptions: 'SAMEORIGIN',
noSniff: true,
dnsPrefetchControl: 'on',
referrerPolicy: 'strict-origin-when-cross-origin',
permissionsPolicy: {
camera: [],
microphone: ['self'],
geolocation: [],
'interest-cohort': [], // Disable FLoC
payment: [],
usb: [],
},
}
Header-Referenz
| Header | Standardwert | Zweck |
|---|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains; preload | Erzwingt HTTPS für 1 Jahr, inkl. Subdomains |
X-Frame-Options | SAMEORIGIN (Seiten) / DENY (API) | Verhindert Clickjacking durch Kontrolle des Iframe-Einbettens |
X-Content-Type-Options | nosniff | Verhindert MIME-Sniffing von Antworten durch Browser |
X-DNS-Prefetch-Control | on | Aktiviert DNS-Prefetching für verlinkte Domains |
X-XSS-Protection | 1; mode=block | Legacy-XSS-Filter für ältere Browser |
Referrer-Policy | strict-origin-when-cross-origin | Steuert den Referrer-Header bei Cross-Origin-Anfragen |
Permissions-Policy | camera=(), microphone=(self), geolocation=(), interest-cohort=(), payment=(), usb=() | Schränkt Browser-APIs ein — Mikrofon für Same-Origin-Audioeingabe erlaubt |
X-Permitted-Cross-Domain-Policies | none | Verhindert das Laden von Daten durch Adobe Flash/PDF |
Cross-Origin-Opener-Policy | same-origin-allow-popups | Isoliert den Browsing-Kontext (Spectre-Schutz) |
Cross-Origin-Resource-Policy | same-site (Seiten) / same-origin (API) | Verhindert das Lesen von Ressourcen durch andere Origins |
Der
Strict-Transport-Security-Header wird nur hinzugefügt, wenn NODE_ENV=production und die Anfrage HTTPS verwendet. Das verhindert Probleme bei der lokalen Entwicklung über HTTP. In der Produktion mit HTTPS ist HSTS immer aktiv.Routenbewusste Headers
Einige Headers verwenden unterschiedliche Richtlinien je nach Routentyp. API-Routen erhalten strengere Einstellungen, da sie niemals eingebettet oder von externen Origins aufgerufen werden sollten:
| Header | Seiten-Routen (/, /dashboard) | API-Routen (/api/*) |
|---|---|---|
X-Frame-Options | SAMEORIGIN — ermöglicht Einbettung in Same-Origin-Iframes | DENY — kein Iframe-Einbetten überhaupt |
Cross-Origin-Resource-Policy | same-site — zugänglich von derselben Site | same-origin — nur von der exakt gleichen Origin zugänglich |
Diese Unterscheidung stellt sicher, dass deine Marketing-Seiten und das Dashboard intern Iframes verwenden können (z. B. für eingebettete Widgets), während API-Endpunkte vollständig abgeschottet sind.
src/lib/security/security-headers.ts — Route-Aware X-Frame-Options
// 2. X-Frame-Options
// API routes: DENY (no framing)
// Other routes: SAMEORIGIN (same-origin framing only)
if (pathname.startsWith('/api/')) {
headers.set('X-Frame-Options', 'DENY')
} else {
const xFrameOptions = mergedConfig.xFrameOptions
if (typeof xFrameOptions === 'string') {
headers.set('X-Frame-Options', xFrameOptions)
} else {
headers.set('X-Frame-Options', `ALLOW-FROM ${xFrameOptions.allow}`)
}
}
CORS-Konfiguration
Cross-Origin Resource Sharing (CORS) steuert, welche externen Domains Anfragen an deine API stellen können. Das Kit definiert drei CORS-Profile basierend auf dem Routentyp:
src/lib/security/cors-middleware.ts — CORS Configurations
const CORS_CONFIGS: Record<string, CORSConfig> = {
// Public API routes - Open CORS for public endpoints
public: {
allowedOrigins: ['*'],
allowedMethods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: false,
maxAge: 86400, // 24 hours
},
// Protected API routes - Controlled origins
api: {
allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',') || [
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
],
allowedMethods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400,
},
// Webhook routes - Accept from any origin (webhooks don't send Origin header)
webhook: {
allowedOrigins: ['*'],
allowedMethods: ['POST', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-Signature',
'Webhook-Signature',
'Svix-Id',
'Svix-Timestamp',
'Svix-Signature',
],
credentials: false,
maxAge: 3600, // 1 hour
},
}
CORS-Profil-Übersicht
| Profil | Routen | Erlaubte Origins | Methoden | Credentials | Max Age |
|---|---|---|---|---|---|
| public | /api/pricing, /api/health, Nicht-API-Routen | * (beliebige Origin) | GET, POST, OPTIONS | Nein | 24 Stunden |
| api | /api/* (geschützte Endpunkte) | ALLOWED_ORIGINS Env-Variable oder App-URL | GET, POST, PATCH, DELETE, OPTIONS | Ja | 24 Stunden |
| webhook | /api/webhooks/* | * (beliebige Origin) | POST, OPTIONS | Nein | 1 Stunde |
So funktioniert die Routentyp-Erkennung
Die CORS-Middleware bestimmt das Profil, indem sie den Anfrage-Pfadnamen der Reihe nach prüft:
src/lib/security/cors-middleware.ts — Route Type Detection
function getRouteType(pathname: string): keyof typeof CORS_CONFIGS {
if (pathname.startsWith('/api/webhooks/')) {
return 'webhook'
}
if (
pathname.startsWith('/api/pricing') ||
pathname.startsWith('/api/health')
) {
return 'public'
}
if (pathname.startsWith('/api/')) {
return 'api'
}
return 'public' // Default to public for non-API routes
}
Die erste passende Regel gewinnt. Webhook-Routen werden vor allgemeinen API-Routen geprüft, da
/api/webhooks/ eine Teilmenge von /api/ ist.CORS-Umgebungsvariablen
Geschützte API-Routen verwenden die Umgebungsvariable
ALLOWED_ORIGINS, um zu bestimmen, welche Domains Cross-Origin-Anfragen stellen dürfen:bash
# Single origin
ALLOWED_ORIGINS=https://myapp.com
# Multiple origins (comma-separated)
ALLOWED_ORIGINS=https://myapp.com,https://staging.myapp.com,https://admin.myapp.com
# Wildcard subdomains are supported in the origin matching
# (configured in code, not via env var)
Wenn
ALLOWED_ORIGINS nicht gesetzt ist, fällt das System auf NEXT_PUBLIC_APP_URL zurück (oder http://localhost:3000, falls das ebenfalls fehlt). Das bedeutet, die lokale Entwicklung funktioniert ohne jede CORS-Konfiguration sofort.Während der lokalen Entwicklung ist CORS kein Problem, da Browser-Anfragen von
localhost:3000 an localhost:3000 Same-Origin sind. CORS gilt nur für Cross-Origin-Anfragen — zum Beispiel ein Frontend auf localhost:3001, das eine API auf localhost:3000 aufruft. Um CORS lokal zu testen, starte Frontend und API auf unterschiedlichen Ports.Preflight-Handling
Die CORS-Middleware fängt
OPTIONS-Anfragen (CORS-Preflight) ab und gibt eine 204 No Content-Antwort mit den entsprechenden CORS-Headers zurück. Das schließt die Middleware-Chain kurz — die Anfrage erreicht weder die Clerk-Authentifizierung noch deinen Route-Handler:OPTIONS /api/upload HTTP/1.1
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
──────────────────────────────────
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Vary: Origin
Der
Vary: Origin-Header stellt sicher, dass Caches separate Antworten für verschiedene Origins speichern. Das Max-Age von 86400 Sekunden (24 Stunden) bedeutet, dass Browser das Preflight-Ergebnis cachen, was die Anzahl der Preflight-Anfragen reduziert.CORS für neue Routen hinzufügen
Wenn du neue API-Routen hinzufügst, erben diese automatisch das richtige CORS-Profil basierend auf dem Pfad:
/api/webhooks/my-service— Erhält das webhook-Profil (offenes CORS, nur POST)/api/my-feature— Erhält das api-Profil (eingeschränkte Origins, Credentials)/api/health-check— Erhält das api-Profil, sofern du es nicht zur öffentlichen Liste hinzufügst
Um eine neue Route mit dem public-Profil zu versehen, füge sie der Erkennungsfunktion hinzu:
typescript
// In apps/boilerplate/src/lib/security/cors-middleware.ts
function getRouteType(pathname: string): keyof typeof CORS_CONFIGS {
if (pathname.startsWith('/api/webhooks/')) return 'webhook'
if (
pathname.startsWith('/api/pricing') ||
pathname.startsWith('/api/health') ||
pathname.startsWith('/api/my-public-endpoint') // Add here
) {
return 'public'
}
if (pathname.startsWith('/api/')) return 'api'
return 'public'
}
Öffentliche CORS-Routen erlauben Anfragen von beliebigen Origins ohne Credentials. Füge keine authentifizierten Endpunkte zum öffentlichen CORS-Profil hinzu — die Einstellung
credentials: false bedeutet, dass Cookies und Auth-Header bei Cross-Origin-Anfragen nicht gesendet werden, was die Authentifizierung unterbricht.API-Key-Verwaltung
Das Kit verwendet mehrere Drittanbieter-API-Keys, die regelmäßig rotiert werden sollten, um den Schaden bei kompromittierten Zugangsdaten zu minimieren:
Rotationsplan
| Key | Rotationszeitraum | Priorität | Dienst |
|---|---|---|---|
CLERK_SECRET_KEY | 90 Tage | Kritisch | Authentifizierung |
LEMON_SQUEEZY_API_KEY | 90 Tage | Kritisch | Zahlungen |
RESEND_API_KEY | 90 Tage | Kritisch | |
UPSTASH_REDIS_REST_TOKEN | 180 Tage | Standard | Rate Limiting / Caching |
BLOB_READ_WRITE_TOKEN | 180 Tage | Standard | Dateispeicher |
OPENAI_API_KEY | 180 Tage | Standard | AI-Anbieter |
ANTHROPIC_API_KEY | 180 Tage | Standard | AI-Anbieter |
GOOGLE_GENERATIVE_AI_API_KEY | 180 Tage | Standard | AI-Anbieter |
Kritische Keys (90-Tage-Rotation) sind Keys, die bei Kompromittierung direkt zu finanziellem Schaden oder der Offenlegung von Nutzerdaten führen können. Standard-Keys (180-Tage-Rotation) haben einen engeren Wirkungsbereich.
Rotationsprozess
- Neuen Key im Dashboard des Anbieters generieren
- Den neuen Key zur Deployment-Umgebung hinzufügen (Vercel etc.)
- Mit dem neuen Key deployen — der alte Key bleibt während der Übergangsphase gültig
- Prüfen, ob der neue Key in der Produktion funktioniert (Logs auf Auth-Fehler prüfen)
- Den alten Key im Dashboard des Anbieters widerrufen
Die meisten Anbieter erlauben mehrere gleichzeitig aktive API-Keys. Das ermöglicht Zero-Downtime-Rotation: neuen Key hinzufügen, deployen, verifizieren, dann den alten widerrufen. Den alten Key niemals widerrufen, bevor bestätigt ist, dass der neue in der Produktion funktioniert.
Sicherheitsrelevante Umgebungsvariablen
| Variable | Erforderlich | Standard | Zweck |
|---|---|---|---|
ALLOWED_ORIGINS | Nein | NEXT_PUBLIC_APP_URL | Kommagetrennte Liste erlaubter CORS-Origins für geschützte API-Routen |
NEXT_PUBLIC_APP_URL | Ja | http://localhost:3000 | Anwendungs-URL — als CORS-Fallback und in Security-Headers verwendet |
CRON_SECRET | Nein | — | Secret zur Authentifizierung von Cron-Job-Anfragen (/api/cron/*) |
CLERK_WEBHOOK_SECRET | Ja | — | SVIX-Secret zur Verifizierung von Clerk-Webhook-Signaturen |
LEMON_SQUEEZY_WEBHOOK_SECRET | Ja | — | Secret zur Verifizierung von Lemon-Squeezy-Webhook-Signaturen |
Wichtige Dateien
| Datei | Zweck |
|---|---|
apps/boilerplate/src/lib/security/security-headers.ts | Generierung und Anwendung von Security-Headers |
apps/boilerplate/src/lib/security/cors-middleware.ts | CORS-Konfiguration, Preflight-Handling und Antwort-Headers |
apps/boilerplate/src/middleware.ts | Middleware-Chain, die Headers und CORS auf jede Anfrage anwendet |