# 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 <CRON_SECRET>` 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 |
