# Security-Richtlinien - Payload CMS Multi-Tenant > Letzte Aktualisierung: 29.12.2025 ## Ü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 | ### 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 | | `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 ### 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 | --- ## Empfehlungen ### Production Checklist - [ ] **`TRUST_PROXY=true`** setzen (Pflicht hinter Reverse-Proxy wie Caddy) - [ ] **`CSRF_SECRET`** oder **`PAYLOAD_SECRET`** setzen (Server startet nicht ohne) - [ ] Alle `BLOCKED_IPS` für bekannte Angreifer setzen - [ ] `SEND_EMAIL_ALLOWED_IPS` auf vertrauenswürdige IPs beschränken - [ ] `ADMIN_ALLOWED_IPS` auf Office/VPN-IPs 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 --- ## 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 | |-------|----------| | 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 |