# Security-Richtlinien - Payload CMS Multi-Tenant > Letzte Aktualisierung: 17.02.2026 ## Übersicht Dieses Dokument beschreibt die implementierten Sicherheitsmaßnahmen für das Payload CMS Multi-Tenant-Projekt. **Umgebungen:** | Umgebung | URL | TRUST_PROXY | |----------|-----|-------------| | Production | https://cms.c2sgmbh.de | `true` (Nginx) | | Staging | https://pl.porwoll.tech | `true` (Caddy) | --- ## Security-Module Alle Security-Funktionen befinden sich in `src/lib/security/`: | Modul | Datei | Zweck | |-------|-------|-------| | Rate Limiter | `rate-limiter.ts` | Schutz vor API-Missbrauch | | IP Allowlist | `ip-allowlist.ts` | IP-basierte Zugriffskontrolle | | CSRF Protection | `csrf.ts` | Cross-Site Request Forgery Schutz | | Data Masking | `data-masking.ts` | Sensitive Daten in Logs maskieren | | Cron Auth | `cron-auth.ts` | Verbindliche Authentifizierung für Cron-Endpunkte | | API Guards | `api-guards.ts` | Zentrale Guards für Auth/IP/CSRF/Rate-Limit | | Cron Coordination | `cron-coordination.ts` | Multi-Instance-Lock + Idempotency für Cron-Ausführung | | Security Observability | `security-observability.ts` | Strukturierte Security-Events, Metriken, Alert-Schwellen | | Secrets Health | `secrets-health.ts` | Monitoring für fehlende/ablaufende/überfällige Secrets | ### Rate Limiter **Vordefinierte Limiter:** | Name | Limit | Fenster | Verwendung | |------|-------|---------|------------| | `publicApiLimiter` | 60 Requests | 1 Minute | Öffentliche API-Endpunkte | | `authLimiter` | 5 Requests | 15 Minuten | Login-Versuche | | `emailLimiter` | 10 Requests | 1 Minute | E-Mail-Versand | | `searchLimiter` | 30 Requests | 1 Minute | Suche & Posts-API | | `formLimiter` | 5 Requests | 10 Minuten | Formular-Submissions | **Verwendung:** ```typescript import { searchLimiter, rateLimitHeaders } from '@/lib/security' export async function GET(req: NextRequest) { const ip = getClientIp(req.headers) const rateLimit = await searchLimiter.check(ip) if (!rateLimit.allowed) { return NextResponse.json( { error: 'Too many requests' }, { status: 429, headers: rateLimitHeaders(rateLimit, 30) } ) } // ... } ``` **Redis-Support:** - Automatischer Fallback auf In-Memory-Store wenn Redis nicht verfügbar - Für verteilte Systeme: Redis via `REDIS_URL` konfigurieren ### IP Allowlist/Blocklist > **WICHTIG: TRUST_PROXY Konfiguration** > > Wenn die Anwendung hinter einem Reverse-Proxy (Caddy, Nginx, etc.) läuft, > **muss** `TRUST_PROXY=true` gesetzt werden. Ohne diese Einstellung werden > `X-Forwarded-For` und `X-Real-IP` Header ignoriert, um IP-Spoofing zu verhindern. > > ```bash > # In .env setzen wenn hinter Reverse-Proxy: > TRUST_PROXY=true > ``` > > Ohne `TRUST_PROXY=true` wird für alle Requests `direct-connection` als IP verwendet, > was Rate-Limiting und IP-Allowlists/-Blocklists ineffektiv macht. **Environment-Variablen:** | Variable | Zweck | Format | |----------|-------|--------| | `TRUST_PROXY` | Proxy-Header vertrauen | `true` oder leer | | `BLOCKED_IPS` | Globale Blocklist | IP, CIDR, Wildcard | | `SEND_EMAIL_ALLOWED_IPS` | E-Mail-Endpoint Allowlist | IP, CIDR, Wildcard | | `GENERATE_PDF_ALLOWED_IPS` | PDF-Endpoint Allowlist | IP, CIDR, Wildcard | | `ADMIN_ALLOWED_IPS` | Admin-Panel Allowlist | IP, CIDR, Wildcard | | `WEBHOOK_ALLOWED_IPS` | Webhook-Endpoint Allowlist | IP, CIDR, Wildcard | **Unterstützte Formate:** ```bash # Einzelne IP BLOCKED_IPS=192.168.1.100 # Mehrere IPs BLOCKED_IPS=192.168.1.100,10.0.0.50 # CIDR-Notation BLOCKED_IPS=10.0.0.0/24 # Wildcard BLOCKED_IPS=192.168.* # Kombiniert BLOCKED_IPS=10.0.0.0/8,192.168.1.50,172.16.* ``` **Verwendung:** ```typescript import { validateIpAccess } from '@/lib/security' const ipCheck = validateIpAccess(req, 'sendEmail') if (!ipCheck.allowed) { return NextResponse.json( { error: 'Access denied', reason: ipCheck.reason }, { status: 403 } ) } ``` ### CSRF Protection **Pattern:** Double Submit Cookie **Token-Endpoint:** `GET /api/csrf-token` > **WICHTIG: CSRF_SECRET in Production** > > In Production **muss** entweder `CSRF_SECRET` oder `PAYLOAD_SECRET` konfiguriert sein. > Ohne Secret startet der Server nicht. Ein vorhersagbares Secret würde Angreifern > erlauben, gültige CSRF-Tokens offline zu generieren. > > ```bash > # In .env setzen: > CSRF_SECRET=mindestens-32-zeichen-langes-geheimnis > # ODER PAYLOAD_SECRET wird als Fallback verwendet > ``` **Frontend-Integration:** ```typescript // 1. Token abrufen const res = await fetch('/api/csrf-token', { credentials: 'include' }) const { csrfToken } = await res.json() // 2. Bei Mutations mitschicken await fetch('/api/send-email', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrfToken, }, body: JSON.stringify({ /* ... */ }) }) ``` **Server-seitige Validierung:** ```typescript import { validateCsrf } from '@/lib/security' const csrf = validateCsrf(req) if (!csrf.valid) { return NextResponse.json( { error: 'CSRF validation failed' }, { status: 403 } ) } ``` **Bypass für Server-to-Server:** - Requests ohne `Origin`/`Referer` Header werden als Server-to-Server behandelt - API-Key-basierte Authentifizierung umgeht CSRF-Check ### Cron Endpoint Auth Alle `/api/cron/*` Endpunkte sind fail-closed abgesichert: - Ohne gültigen `Authorization: Bearer ` Header: `401` - Ohne gesetztes `CRON_SECRET`: `503` - Gilt für `GET`, `POST` und `HEAD` - Mit `withCronExecution(...)`: - Verteiltes Execution-Lock (Redis, In-Memory-Fallback) - Idempotency-Key-Schutz über `x-idempotency-key` oder `?idempotencyKey=...` - Doppelte Trigger werden mit `202` ignoriert In Production wird `CRON_SECRET` beim Startup validiert. ### PDF URL Hardening (SSRF-Schutz) PDF-Generierung per URL (`/api/generate-pdf`) validiert jetzt strikt: - Nur `https://` URLs (Ausnahme: `PDF_ALLOW_HTTP_URLS=true` nur non-production) - Keine URL-Credentials (`user:pass@host`) - Keine localhost/private/loopback Ziele - DNS-Auflösung wird geprüft (Rebinding-Schutz gegen private Zieladressen) - Optionaler Host-Allowlist-Modus via `PDF_ALLOWED_HOSTS` Beispiel: ```bash PDF_ALLOWED_HOSTS=example.com,.example.com PDF_ALLOW_HTTP_URLS=false ``` Geblockte SSRF-Versuche erzeugen Security-Events (`pdf_ssrf_blocked`) und können Alert-Schwellen auslösen. ### Zentrale API Guards und Request-Validierung `runApiGuards(...)` bündelt folgende Checks konsistent: - IP-Blocklist / IP-Allowlist - CSRF (browser-basiert, server-to-server aware) - Authentifizierung (`payload.auth`) - Rate-Limiting inkl. standardisierter 429-Antwort Die Body-Validierung läuft über `src/lib/validation/api-validation.ts` mit: - einheitlichen Fehlercodes (`required`, `invalid_type`, `invalid_value`, `invalid_json`) - strukturierter Fehlerantwort (`VALIDATION_FAILED`) - optionalem Security-Event (`request_validation_failed`) ### Security Observability (Logs, Metriken, Alerts) Security-relevante Blockierungen werden zentral erfasst: - `cron_auth_rejected`, `cron_secret_missing` - `cron_execution_locked`, `cron_duplicate_ignored` - `pdf_ssrf_blocked` - `rate_limit_blocked` - `ip_access_denied` - `csrf_blocked` - `auth_blocked` - `request_validation_failed` Metrik-/Alert-Konfiguration: ```bash SECURITY_METRICS_WINDOW_MS=300000 SECURITY_ALERT_COOLDOWN_MS=900000 SECURITY_ALERT_THRESHOLD_DEFAULT=25 SECURITY_ALERT_THRESHOLD_CRON_AUTH_REJECTED=10 SECURITY_ALERT_THRESHOLD_PDF_SSRF_BLOCKED=5 SECURITY_ALERT_THRESHOLD_RATE_LIMIT_BLOCKED=50 ``` Bei Schwellwert-Verletzungen werden Alerts über den vorhandenen Alert-Service dispatcht. ### Secret Lifecycle Monitoring Zusätzlich zur Startup-Validierung werden Secret-Risiken im Monitoring-Snapshot erfasst: - fehlende Pflicht-Secrets (`missing`) - ablaufende Secrets (`expiringSoon`) - abgelaufene Secrets (`expired`) - überfällige Rotation (`rotationOverdue`) Relevante Variablen: ```bash SECRET_EXPIRY_WARNING_DAYS=14 SECRET_ROTATION_MAX_DAYS=90 PAYLOAD_SECRET_ROTATED_AT=2026-02-01T00:00:00Z PAYLOAD_SECRET_EXPIRES_AT=2026-08-01T00:00:00Z CRON_SECRET_ROTATED_AT=2026-02-01T00:00:00Z CRON_SECRET_EXPIRES_AT=2026-08-01T00:00:00Z ``` Wenn `*_EXPIRES_AT` gesetzt und bereits abgelaufen ist, schlägt der Serverstart in Production fehl (fail-closed). Runbook: `docs/anleitungen/SECRET_ROTATION_RUNBOOK.md` ### Data Masking **Automatisch maskierte Felder:** - password, passwd, pwd - token, accessToken, refreshToken - apiKey, api_key - secret, clientSecret - credentials - privateKey, private_key - smtpPassword, smtp_pass **Verwendung:** ```typescript import { maskObject, createSafeLogger } from '@/lib/security' // Objekte maskieren const safeData = maskObject(sensitiveObject) // Safe Logger verwenden const logger = createSafeLogger('MyModule') logger.info('User action', { password: 'secret' }) // Output: { password: '[REDACTED]' } ``` **Pattern-Erkennung:** - JWT Tokens: Header bleibt, Payload/Signature maskiert - Connection Strings: Passwort maskiert - Private Keys: Komplett ersetzt --- ## Pre-Commit Hook Secret Detection bei jedem Commit via `scripts/detect-secrets.sh`: **Installation:** ```bash ln -sf ../../scripts/detect-secrets.sh .git/hooks/pre-commit ``` **Erkannte Patterns:** - API Keys und Tokens - AWS Credentials - Private Keys - Passwörter in Code - SMTP Credentials - Database Connection Strings - JWT Tokens - Webhook URLs (Slack, Discord) - GitHub/SendGrid/Stripe Tokens **Ignorierte Dateien:** - `*.min.js`, `*.min.css` - `package-lock.json`, `pnpm-lock.yaml` - `*.md`, `*.txt` - `*.example`, `*.sample` - `*.spec.ts`, `*.test.ts` (Test-Dateien) --- ## CI/CD Security **.github/workflows/security.yml:** | Job | Prüfung | |-----|---------| | `secrets` | Gitleaks Secret Scanning | | `dependencies` | npm audit, Dependency Check | | `codeql` | Static Code Analysis | | `security-tests` | 177 Security Unit & Integration Tests | --- ## Test Suite **Ausführung:** ```bash # Alle Security-Tests pnpm test:security # Nur Unit-Tests pnpm test:unit ``` **Abdeckung:** | Test-Datei | Tests | Bereich | |------------|-------|---------| | `rate-limiter.unit.spec.ts` | 24 | Limiter, Tracking, Reset, TRUST_PROXY | | `csrf.unit.spec.ts` | 34 | Token, Validierung, Origin | | `ip-allowlist.unit.spec.ts` | 35 | CIDR, Wildcards, Endpoints, TRUST_PROXY | | `data-masking.unit.spec.ts` | 41 | Felder, Patterns, Rekursion | | `security-api.int.spec.ts` | 33 | API-Integration | | `cron-auth.unit.spec.ts` | neu | Cron-Auth, Idempotency, Lock | | `pdf-url-validation.unit.spec.ts` | neu | SSRF-Validierung | | `users-access.unit.spec.ts` | neu | Users-Update/Field Access Regression | | `newsletter-unsubscribe.unit.spec.ts` | neu | Token-Rotation & Replay-Schutz | --- ## Empfehlungen ### Production Checklist - [ ] **`TRUST_PROXY=true`** setzen (Pflicht hinter Reverse-Proxy wie Caddy) - [ ] **`CSRF_SECRET`** oder **`PAYLOAD_SECRET`** setzen (Server startet nicht ohne) - [ ] **`CRON_SECRET`** setzen (Pflicht für Cron-Endpunkte in Production) - [ ] **`SCHEDULER_MODE=external`** in Production setzen - [ ] **`CRON_LOCK_TTL_MS`** und **`CRON_IDEMPOTENCY_TTL_MS`** prüfen - [ ] Alle `BLOCKED_IPS` für bekannte Angreifer setzen - [ ] `SEND_EMAIL_ALLOWED_IPS` auf vertrauenswürdige IPs beschränken - [ ] `GENERATE_PDF_ALLOWED_IPS` auf vertrauenswürdige IPs beschränken - [ ] `ADMIN_ALLOWED_IPS` auf Office/VPN-IPs setzen - [ ] `PDF_ALLOWED_HOSTS` für erlaubte externe Render-Ziele konfigurieren - [ ] `ENABLE_IN_PROCESS_SCHEDULER` nur für lokale Entwicklung aktivieren - [ ] `SECRET_EXPIRY_WARNING_DAYS` / `SECRET_ROTATION_MAX_DAYS` passend setzen - [ ] Redis für verteiltes Rate Limiting konfigurieren - [ ] Pre-Commit Hook aktivieren ### Monitoring - Rate Limit Violations loggen (429 Responses) - CSRF Validation Failures überwachen - Blocked IP Attempts tracken - Login-Fehlversuche (authLimiter) alertieren - Cron-Auth-Rejections und Cron-Lock-Kollisionen alertieren - SSRF-Blocks bei PDF-Generierung monitoren - Secret-Lifecycle (`external.secrets`) täglich prüfen ### Repo-History-Scan Für History-Scans steht `scripts/security/history-scan.sh` bereit: ```bash ./scripts/security/history-scan.sh ``` Aktueller Befund: `docs/reports/2026-02-17-history-scan.md` --- ## Custom Login Route Das Admin Panel verwendet eine Custom Login Route (`src/app/(payload)/api/users/login/route.ts`) mit folgenden Features: - **Audit-Logging:** Jeder Login-Versuch wird in AuditLogs protokolliert - **Rate-Limiting:** 5 Versuche pro 15 Minuten (authLimiter) - **Browser-Redirect:** Sichere Weiterleitung nach erfolgreichem Login - **Content-Type Support:** - JSON (`application/json`) - FormData mit `_payload` JSON-Feld (Payload Admin Panel Format) - Standard FormData (`multipart/form-data`) - URL-encoded (`application/x-www-form-urlencoded`) **Browser Form Redirect:** ``` POST /api/users/login?redirect=/admin/collections/posts Content-Type: application/x-www-form-urlencoded email=admin@example.com&password=secret ``` **Redirect-Validierung:** - Nur relative Pfade erlaubt (`/admin/...`) - Externe URLs werden blockiert - Protocol-Handler (`javascript:`, `data:`) abgelehnt - Default: `/admin` bei fehlendem/ungültigem Redirect **Sicherheitsaspekte:** - Passwort wird nie in Logs/Responses exponiert - Fehlgeschlagene Login-Versuche werden mit IP und User-Agent geloggt - Rate-Limiting verhindert Brute-Force-Angriffe - Open Redirect Prevention durch URL-Validierung --- ## Änderungshistorie | Datum | Änderung | |-------|----------| | 17.02.2026 | **Security-Hardening:** Users-Update fail-closed, `isSuperAdmin` field-protected, Cron-Auth fail-closed, PDF-SSRF-Schutz, Newsletter-Unsubscribe ohne ID-Enumeration | | 29.12.2025 | **Dokumentation aktualisiert:** Custom Login Page Abschnitt entfernt (wurde am 27.12.2025 entfernt) | | 17.12.2025 | **Security-Audit Fixes:** TRUST_PROXY für IP-Header-Spoofing, CSRF_SECRET Pflicht in Production, IP-Allowlist Startup-Warnungen, Tests auf 177 erweitert | | 09.12.2025 | Custom Login Route Dokumentation, multipart/form-data _payload Support | | 08.12.2025 | Security Test Suite (143 Tests) | | 07.12.2025 | Rate Limiter, CSRF, IP Allowlist, Data Masking | | 07.12.2025 | Pre-Commit Hook, GitHub Actions Workflow | --- ## Dateien | Pfad | Beschreibung | |------|--------------| | `src/lib/security/rate-limiter.ts` | Rate Limiting mit Redis/Memory | | `src/lib/security/ip-allowlist.ts` | IP-basierte Zugriffskontrolle | | `src/lib/security/csrf.ts` | CSRF Token Generation & Validation | | `src/lib/security/data-masking.ts` | Sensitive Data Masking | | `src/app/(payload)/api/users/login/route.ts` | Custom Login API mit Audit | | `scripts/detect-secrets.sh` | Pre-Commit Secret Detection | | `.github/workflows/security.yml` | CI Security Scanning | | `tests/unit/security/` | Security Unit Tests | | `tests/int/security-api.int.spec.ts` | Security Integration Tests |