- Add environment table (Production/Staging URLs with TRUST_PROXY) - Document browser form redirect with safe URL validation - Add Open Redirect Prevention details - Document custom admin login page (src/app/(payload)/admin/login/) - Add file reference table for all security-related files - Update changelog with 18.12.2025 entry 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
10 KiB
Security-Richtlinien - Payload CMS Multi-Tenant
Letzte Aktualisierung: 18.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:
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_URLkonfigurieren
IP Allowlist/Blocklist
WICHTIG: TRUST_PROXY Konfiguration
Wenn die Anwendung hinter einem Reverse-Proxy (Caddy, Nginx, etc.) läuft, muss
TRUST_PROXY=truegesetzt werden. Ohne diese Einstellung werdenX-Forwarded-ForundX-Real-IPHeader ignoriert, um IP-Spoofing zu verhindern.# In .env setzen wenn hinter Reverse-Proxy: TRUST_PROXY=trueOhne
TRUST_PROXY=truewird für alle Requestsdirect-connectionals 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:
# 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:
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_SECREToderPAYLOAD_SECRETkonfiguriert sein. Ohne Secret startet der Server nicht. Ein vorhersagbares Secret würde Angreifern erlauben, gültige CSRF-Tokens offline zu generieren.# In .env setzen: CSRF_SECRET=mindestens-32-zeichen-langes-geheimnis # ODER PAYLOAD_SECRET wird als Fallback verwendet
Frontend-Integration:
// 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:
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/RefererHeader 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:
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:
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.csspackage-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:
# 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=truesetzen (Pflicht hinter Reverse-Proxy wie Caddy)CSRF_SECREToderPAYLOAD_SECRETsetzen (Server startet nicht ohne)- Alle
BLOCKED_IPSfür bekannte Angreifer setzen SEND_EMAIL_ALLOWED_IPSauf vertrauenswürdige IPs beschränkenADMIN_ALLOWED_IPSauf 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
_payloadJSON-Feld (Payload Admin Panel Format) - Standard FormData (
multipart/form-data) - URL-encoded (
application/x-www-form-urlencoded)
- JSON (
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:
/adminbei 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
Custom Admin Login Page
Eine optionale Custom Login-Seite ist verfügbar unter src/app/(payload)/admin/login/:
src/app/(payload)/admin/login/
├── page.tsx # Login-Formular mit Styling
└── page.module.scss # Custom Styles
Features:
- Styled Login-Form passend zum Admin-Theme
- Redirect-Parameter Support (
?redirect=/admin/...) - Fehlerbehandlung mit User-Feedback
- Kompatibel mit Payload's Session-Management
Änderungshistorie
| Datum | Änderung |
|---|---|
| 18.12.2025 | Custom Admin Login Page: Styled Login-Formular, Browser-Redirect mit Safe-URL-Validierung, Open Redirect Prevention |
| 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 |
src/app/(payload)/admin/login/page.tsx |
Custom Login Page |
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 |