GET /api/tenants/{id}/logo prüfte nur die Authentifizierung, aber keinen
Tenant-Scope — jeder eingeloggte Nutzer konnte das Logo jedes beliebigen
Tenants lesen. Kombiniert mit dem bisher erlaubten SVG-Upload (kann
eingebettetes JavaScript enthalten) ergab das einen Cross-Tenant Stored-XSS:
ein domain_admin konnte ein bösartiges SVG als eigenes Logo hochladen und
Opfer aus beliebigen anderen Tenants per direktem Link darauf locken.
Fix: tenantAccessAllowed()-Scope-Check beim Logo-Lesepfad (analog PROJ-55),
SVG aus erlaubten Upload-Typen entfernt, X-Content-Type-Options: nosniff
als Defense-in-Depth ergänzt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.9 KiB
PROJ-61: Fix Cross-Tenant Stored XSS via Mandanten-Logo (Sicherheitsbug)
Status: Deployed
Created: 2026-06-25 Last Updated: 2026-06-25
Hintergrund (Security-Audit, 2026-06-25)
Nachtrag zum letzten Audit (docs/security-audit-2026-03-18.md) über alle Commits seit März (PROJ-25–60). Zwei kombinierte Lücken in den Tenant-Logo-Handlern (eingeführt mit Multi-Tenancy-Ausbau):
POST /api/tenant/logo(eigener Tenant,domain_adminreicht) akzeptiertimage/svg+xmlohne Sanitisierung. SVG kann<script>/onloadenthalten.GET /api/tenants/{id}/logoprüft nurs.auth(jeder eingeloggte Nutzer, egal welcher Tenant/Rolle) — kein Tenant-Scope-Check, anders als die Schreib-/Lösch-Pendants (requireRole/authAdminprüfen nur Rollenlevel, keinen Tenant-Bezug).
Exploit-Pfad: domain_admin von Tenant A lädt präpariertes SVG als eigenes Logo hoch → lockt ein Opfer aus einem BELIEBIGEN anderen Tenant per direktem Link auf /api/tenants/A/logo → SVG wird same-origin mit image/svg+xml ausgeliefert und im Browser des Opfers ausgeführt → Skript kann mit dem httpOnly-JWT-Cookie des Opfers API-Calls im Namen des Opfers auslösen.
Acceptance Criteria
GET /api/tenants/{id}/logoerzwingt Tenant-Scope: nursuperadmin/admin(global) ODER der Nutzer gehört zum angefragten Tenant — analog zum projektüblichentenantAccessAllowed()-Muster (siehe PROJ-55).- SVG (
image/svg+xml) wird beim Logo-Upload nicht mehr akzeptiert (einfachste, robusteste Lösung). - Bestehende, bereits hochgeladene Logos werden beim Ausliefern zusätzlich defensiv abgesichert:
X-Content-Type-Options: nosniffHeader bei Logo-Responses ergänzt (Defense-in-Depth, auch für nicht-SVG-Typen). - Bestehende Funktionalität (PNG/JPEG/GIF/WebP-Logos hoch-/runterladen, eigene + fremde Tenant-Logos für globale Admins) bleibt unverändert nutzbar.
- Kein Datenverlust: bereits gespeicherte SVG-Logos werden nicht automatisch gelöscht, nur zukünftige Uploads blockiert (siehe Restrisiko unten).
Tech Design
Übersprungen (klar umrissener Sicherheitsfix mit bestehenden Mustern, kein architektonischer Schnitt).
Implementation Notes (2026-06-25)
Geändert: internal/api/tenant_logo_handlers.go, internal/api/ldap_tenants.go.
-
IDOR-Fix (
handleGetTenantLogo): Nach dem Parsen deridwirdsessionFromCtx(r.Context())geholt undtenantAccessAllowed(sess, &id)geprüft. Globale Sessions (sess.TenantID == nil, also superadmin/admin) dürfen jedes Tenant-Logo lesen; alle anderen nur das eigene (*sess.TenantID == id), sonst HTTP 403. Route inldap_tenants.gobleibts.auth(...)(setzt Session via authMiddleware), Scope jetzt im Handler erzwungen — Kommentar angepasst. Kein neuer Import nötig (sessionFromCtx/tenantAccessAllowedsind im selben Package). -
XSS-Fix (
saveTenantLogo):"image/svg+xml"aus derallowed-Map entfernt; Fehlertext und erlaubte Typen aktualisiert (png, jpeg, gif, webp). PNG/JPEG/GIF/WebP unverändert. -
Defense-in-Depth:
w.Header().Set("X-Content-Type-Options", "nosniff")inhandleGetTenantLogoundhandleGetOwnTenantLogovor dem Body-Write ergänzt.
Frontend-Prüfung: getTenantLogoUrl(tenantId) wird ausschließlich in src/hooks/useTenantLogos.ts (Superadmin/Admin-Tenant-Verwaltungsdialog) genutzt; das eigene Logo läuft über /api/tenant/logo. Kein Frontend-Pfad liest fremde Tenant-Logos als nicht-globaler Nutzer → IDOR-Fix bricht nichts.
Restrisiko / Offene Fragen
- Bestands-SVGs: Bereits gespeicherte SVG-Logos werden weiter mit
image/svg+xmlausgeliefert.nosniffverhindert MIME-Sniffing, aber NICHT die Ausführung eines explizit alsimage/svg+xmlausgelieferten SVG bei direkter Navigation. Restrisiko ist durch den IDOR-Fix stark reduziert (nur noch eigener Tenant + globale Admins). Falls Bestands-SVGs existieren, sollte ein einmaliges Cleanup/Re-Upload erwogen werden (separate Aufgabe). - Lokal kein
go buildmöglich — Compile auf 192.168.1.131 (devops-deploy) verifizieren.
QA Test Results (192.168.1.132, 2026-06-25)
- Build: Exit 0.
go vet: keine neuen Befunde (einziger Treffer vorbestehend, PROJ-61-fremd:api_test.go:36 os.Discard). - a) superadmin liest beliebiges Tenant-Logo → 200/404, nicht 403. PASS
- b) domain_admin (Tenant 3) liest fremdes Tenant-Logo (1, 2) → 403. Eigenes Tenant (3) → 404 (kein Logo). PASS — IDOR behoben.
- c) domain_admin liest eigenes Logo via
/api/tenant/logo→ normal. PASS - d) SVG-Upload → 400 "unsupported image type". PASS — XSS-Vektor blockiert.
- e) PNG-Upload → 200. PASS — bestehende Funktionalität erhalten.
- f)
X-Content-Type-Options: nosniffim Response-Header bestätigt. PASS - Unauthentifizierter Zugriff → 401. Globale Admins weiterhin uneingeschränkt.
- Keine Regressionen. Cleanup auf 132 durchgeführt, Health-Check OK.
Deployment
- Test (192.168.1.132): QA bestanden, System nach Test zurückgesetzt. 2026-06-25.
- Produktion (192.168.1.131): siehe unten.