Security-Hardening: IDOR-Fixes, XSS-Sanitizer, CORS+Helmet, Data-Exposure
Umfassender Security-Review vor öffentlichem Deployment. Detaillierter Report in docs/SECURITY-REVIEW.md. 🔴 KRITISCHE FIXES: 1. CORS offen → jetzt nur explizite Origins (via CORS_ORIGINS env), in Production per default komplett aus (gleiche Origin erzwingt Browser). 2. Keine Security-Headers → helmet-Middleware hinzugefügt. X-Frame-Options, X-Content-Type-Options, HSTS, Referrer-Policy, CORP. 3. JWT-Fallback-Secret entfernt. Beim Server-Start wird jetzt geprüft ob JWT_SECRET (min 32 Zeichen) und ENCRYPTION_KEY (exakt 64 Hex) gesetzt sind, sonst Fail-Fast mit klarer Fehlermeldung. 4. IDOR bei 7 Contract-Endpoints. Portal-Kunden mit 'contracts:read' konnten über geratene IDs fremde Daten abrufen (Passwort, SIM-PIN/PUK, Internet-Zugangsdaten, SIP-Credentials, Vertragsdokumente, Rechnungen). Neuer Helper canAccessContract() in utils/accessControl.ts in allen betroffenen Endpoints eingebaut. Prüft Vertrag-Besitzer + Vollmachten. 5. XSS via Email-Body. email.htmlBody wurde ungefiltert via dangerouslySetInnerHTML gerendert. Angreifer konnte Mail mit <script> schicken → Token-Diebstahl aus localStorage. Jetzt mit DOMPurify sanitized: verbietet script/iframe/form/inline-handler, erlaubt normale Formatierung + Bilder. 6. Customer-API leakte sensible Felder: - portalPasswordHash (bcrypt-Hash) - portalPasswordEncrypted (symmetrisch, mit ENCRYPTION_KEY entschlüsselbar) - portalPasswordResetToken (gültig 2h) Neuer Sanitizer in utils/sanitize.ts, angewendet in getCustomer/getCustomers. Admin mit customers:update darf portalPasswordEncrypted sehen (für UI-Anzeige), alle anderen Rollen nicht. 🟡 WICHTIGE FIXES: 7. Portal-JWT-Invalidation nach Passwort-Reset. Neues Feld Customer.portalTokenInvalidatedAt, wird beim Reset auf now() gesetzt. Auth-Middleware prüft Portal-Sessions dagegen. Alte Sessions werden dadurch invalidiert. 8. express.json() mit 5 MB Size-Limit (statt Default 100 KB unklar). Neue Files: - backend/src/utils/accessControl.ts - IDOR-Schutz - backend/src/utils/sanitize.ts - Response-Sanitizer - docs/SECURITY-REVIEW.md - vollständiger Report + Deployment-Checkliste Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
# Security-Review vor 1.0.0
|
||||
|
||||
Systematischer Review des Codebase mit Fokus auf Produktions-Hardening
|
||||
vor öffentlichem Deployment (hinter HTTPS-Proxy).
|
||||
|
||||
## Gefundene Probleme & Fixes
|
||||
|
||||
### 🔴 KRITISCH (sofort gefixt)
|
||||
|
||||
#### 1. CORS komplett offen
|
||||
**Vorher:** `app.use(cors())` – jede Origin darf Requests senden.
|
||||
**Risiko:** Fremde Websites können bei eingeloggtem User Requests mit dessen
|
||||
JWT durchführen (wenn Token in Cookies wäre – bei localStorage weniger relevant,
|
||||
aber trotzdem schlechte Praxis).
|
||||
**Fix:** CORS nur für explizit konfigurierte Origins (via `CORS_ORIGINS` ENV),
|
||||
in Production per Default komplett aus (Frontend läuft unter gleicher Origin).
|
||||
|
||||
#### 2. Keine Security-Headers (Helmet fehlt)
|
||||
**Vorher:** Keine HTTP-Security-Headers gesetzt.
|
||||
**Risiko:** XSS, Clickjacking, MIME-Sniffing, Missing HSTS.
|
||||
**Fix:** `helmet`-Middleware aktiviert – setzt automatisch:
|
||||
X-Frame-Options, X-Content-Type-Options, Referrer-Policy, HSTS (in HTTPS),
|
||||
Cross-Origin-Resource-Policy.
|
||||
|
||||
#### 3. JWT-Fallback-Secret
|
||||
**Vorher:** `jwt.verify(token, process.env.JWT_SECRET || 'fallback-secret')`
|
||||
**Risiko:** Wenn `.env` kaputt ist oder Secret leer → bekannter String
|
||||
"fallback-secret" → **Tokens können gefälscht werden!**
|
||||
**Fix:** Beim Server-Start wird geprüft, dass JWT_SECRET mindestens 32 Zeichen lang
|
||||
und ENCRYPTION_KEY exakt 64 Hex-Zeichen hat. Sonst Abbruch mit klarer Fehlermeldung.
|
||||
Fallback wurde aus dem Code entfernt.
|
||||
|
||||
#### 4. IDOR bei sensiblen Contract-Endpoints
|
||||
**Vorher:** Portal-Kunden haben `contracts:read` Permission → können über
|
||||
geratene IDs auf **fremde** Daten zugreifen:
|
||||
- `GET /contracts/:id/password` → Passwort im Klartext
|
||||
- `GET /contracts/simcard/:id/credentials` → PIN/PUK
|
||||
- `GET /contracts/:id/internet-credentials` → Internet-Passwort
|
||||
- `GET /contracts/phonenumber/:id/sip-credentials` → SIP-Passwort
|
||||
- `GET /contracts/:id/documents` → Vertragsdokumente
|
||||
- `GET /contracts/:id/invoices` → Rechnungen
|
||||
- `POST /contracts/:id/invoices` → Rechnung zu fremdem Vertrag hinzufügen
|
||||
**Fix:** Neuer Helper `canAccessContract()` in `backend/src/utils/accessControl.ts`.
|
||||
Wird in allen sensiblen Endpoints aufgerufen und prüft:
|
||||
- Mitarbeiter/Admin → OK
|
||||
- Portal-Kunde + eigener Vertrag → OK
|
||||
- Portal-Kunde + vertretener Kunde MIT gültiger Vollmacht → OK
|
||||
- Sonst 403 Forbidden
|
||||
|
||||
#### 5. XSS via Email-Body
|
||||
**Vorher:** `<div dangerouslySetInnerHTML={{ __html: email.htmlBody }} />`
|
||||
**Risiko:** Ein Angreifer sendet Mail mit `<script>fetch('/api/...')` →
|
||||
wird im Browser des Mitarbeiters ausgeführt → JWT-Token-Diebstahl möglich.
|
||||
**Fix:** DOMPurify sanitized `htmlBody` vor dem Rendern:
|
||||
- Verbietet: script, style, iframe, object, embed, form, inline-handler
|
||||
- Erlaubt: normale Formatierung, Bilder, Links
|
||||
- Zusätzlich: target=_blank damit Links neue Tabs öffnen
|
||||
|
||||
#### 6. Customer-API leakt Passwort-Hashes + Reset-Tokens
|
||||
**Vorher:** `getCustomer` / `getCustomers` gab alle Felder zurück inklusive:
|
||||
- `portalPasswordHash` (bcrypt)
|
||||
- `portalPasswordEncrypted` (symmetrisch, entschlüsselbar mit Key)
|
||||
- `portalPasswordResetToken` (gültig 2h, damit könnte man das Passwort zurücksetzen)
|
||||
**Fix:** Zentrale Sanitizer-Helper in `backend/src/utils/sanitize.ts`:
|
||||
- `sanitizeCustomer` → entfernt Hash + Reset-Token
|
||||
- `sanitizeCustomerStrict` → zusätzlich ohne Encrypted-Passwort
|
||||
(für Nicht-Admin-Rollen)
|
||||
- Im `getCustomer`/`getCustomers` angewendet: Admins sehen encrypted
|
||||
(um Passwort in UI anzeigen zu können), alle anderen nicht.
|
||||
|
||||
### 🟡 WICHTIG (gefixt)
|
||||
|
||||
#### 7. Portal-JWT-Invalidation fehlte
|
||||
**Vorher:** Nach einem Portal-Passwort-Reset blieben alte JWTs bis zum Ablauf (7d) gültig.
|
||||
**Risiko:** Wenn ein Angreifer einen Token geklaut hat, konnte der Kunde das
|
||||
Passwort zwar ändern, aber der Angreifer blieb eingeloggt.
|
||||
**Fix:** Neues Feld `Customer.portalTokenInvalidatedAt` analog zu
|
||||
`User.tokenInvalidatedAt`. Wird bei Portal-Passwort-Reset auf `now()` gesetzt.
|
||||
Auth-Middleware prüft bei Portal-Sessions diesen Timestamp gegen `token.iat`.
|
||||
|
||||
#### 8. express.json() ohne Size-Limit
|
||||
**Vorher:** Default 100KB – aber unklar und nicht explizit.
|
||||
**Fix:** `express.json({ limit: '5mb' })` – deckt normale API-Bodies mit
|
||||
eingebetteten Base64-Attachments ab, blockt aber DoS-Versuche mit 100MB-Payloads.
|
||||
|
||||
## Nicht kritische Findings (Empfehlungen für später)
|
||||
|
||||
### 🟢 Token in Query-Parameter
|
||||
Für Attachment-Downloads/iframes wird das JWT als `?token=...` mitgegeben.
|
||||
**Risiko:** Token landet in Server-Access-Logs, Browser-History, Referer-Headers.
|
||||
**Mitigation aktuell:** JWT läuft nach 7d ab, und bei `password-reset` werden
|
||||
alle Sessions gekickt.
|
||||
**Bessere Lösung (später):** Kurzlebige Download-Tokens (5 Min) statt JWT direkt.
|
||||
|
||||
### 🟢 Upload: nur Browser-MIME-Check
|
||||
Multer prüft nur den vom Browser gesendeten Content-Type. Ein Angreifer könnte
|
||||
eine Shell mit `application/pdf` hochladen.
|
||||
**Mitigation aktuell:**
|
||||
- Uploads-Ordner hat keine Execute-Rechte (Linux-Standard)
|
||||
- Dateien werden mit uniquem Namen + Original-Extension gespeichert
|
||||
- Apache/Caddy served Uploads mit `Content-Disposition: attachment` inline (keine Ausführung)
|
||||
**Besser (später):** Magic-Byte-Check via `file-type` npm-Paket.
|
||||
|
||||
### 🟢 `.env` in git history
|
||||
Die initiale `.env` mit Demo-Secrets ist im ersten Commit eingecheckt.
|
||||
**Risiko:** Wenn das Repo öffentlich wird, sind die Demo-Keys bekannt.
|
||||
**Action:** Vor Öffentlich-Machen: `openssl rand -hex 64` für neuen JWT_SECRET
|
||||
und `openssl rand -hex 32` für neuen ENCRYPTION_KEY in `.env.production`.
|
||||
Optional: `git filter-repo` um `.env` aus History zu löschen.
|
||||
|
||||
## Deployment-Checkliste vor Go-Live
|
||||
|
||||
- [ ] **ENV-Vars setzen:**
|
||||
- `JWT_SECRET` neu generiert (`openssl rand -hex 64`)
|
||||
- `ENCRYPTION_KEY` neu generiert (`openssl rand -hex 32`)
|
||||
- `NODE_ENV=production`
|
||||
- `CORS_ORIGINS=https://crm.meinedomain.de` (oder leer wenn SPA unter gleicher Origin)
|
||||
- `PUBLIC_URL=https://crm.meinedomain.de` (für Reset-Links in E-Mails)
|
||||
- [ ] **Helmet HSTS aktiv** (automatisch mit helmet + HTTPS hinter Caddy)
|
||||
- [ ] **Dependencies aktuell:** `npm audit fix` lauen lassen
|
||||
- [ ] **DB-User minimal:** Prod-User darf nur INSERT/UPDATE/DELETE/SELECT auf opencrm DB,
|
||||
nicht DROP/ALTER/CREATE
|
||||
- [ ] **Uploads-Ordner:** chmod 750, keine Execute-Rechte
|
||||
- [ ] **Backup-Job:** Crontab mit täglichem `npm run db:backup`
|
||||
- [ ] **Log-Rotation:** logrotate für Node-Process-Logs
|
||||
- [ ] **Monitoring:** uptime-kuma o.Ä. auf `/api/health`
|
||||
- [ ] **Reverse-Proxy (Caddy) setzt:**
|
||||
- HSTS (mindestens 1 Jahr)
|
||||
- automatisches SSL via Let's Encrypt
|
||||
- Body-Size-Limit (Caddy-Config)
|
||||
|
||||
## Was getestet werden MUSS (vor öffentlichem Deployment)
|
||||
|
||||
1. **IDOR-Tests:** Als Portal-Kunde A einloggen, fremde IDs per URL/API probieren
|
||||
→ alle müssen 403 geben (siehe TESTING.md)
|
||||
2. **XSS-Tests:** Test-Mail mit `<script>alert(1)</script>` in HTML-Body senden,
|
||||
im Email-Client öffnen → kein Alert
|
||||
3. **Rate-Limit-Tests:** 11x falsch einloggen → muss blocken
|
||||
4. **Password-Reset-Tests:** Reset-Link 2x nutzen → zweites Mal fehlschlägt
|
||||
|
||||
## Übersicht der Code-Änderungen
|
||||
|
||||
| Datei | Änderung |
|
||||
|---|---|
|
||||
| `backend/src/index.ts` | Helmet, CORS-Config, Body-Limit, ENV-Check beim Start |
|
||||
| `backend/src/middleware/auth.ts` | JWT-Fallback raus, Portal-Token-Invalidation |
|
||||
| `backend/src/services/auth.service.ts` | JWT-Fallback raus, `portalTokenInvalidatedAt` setzen |
|
||||
| `backend/src/utils/accessControl.ts` | **NEU** – `canAccessContract`, `canAccessCustomer` |
|
||||
| `backend/src/utils/sanitize.ts` | **NEU** – Sanitizer für Customer/User |
|
||||
| `backend/src/controllers/contract.controller.ts` | IDOR-Schutz in 5 Endpoints |
|
||||
| `backend/src/controllers/invoice.controller.ts` | IDOR-Schutz in 2 Endpoints |
|
||||
| `backend/src/controllers/customer.controller.ts` | Sanitizer in getCustomer/getCustomers |
|
||||
| `backend/prisma/schema.prisma` | `Customer.portalTokenInvalidatedAt` |
|
||||
| `frontend/src/components/email/EmailDetail.tsx` | DOMPurify für htmlBody |
|
||||
Reference in New Issue
Block a user