Files
archivmail/internal/api/ldap_tenants.go
T
sysopsandClaude Sonnet 4.6 363874767b fix(PROJ-61): Cross-Tenant Stored XSS via Mandanten-Logo behoben (Sicherheitsbug)
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>
2026-06-25 00:31:41 +02:00

71 lines
4.6 KiB
Go

package api
import (
ldapcfg "archivmail/internal/ldapconfig"
"archivmail/internal/tenantstore"
"archivmail/internal/userstore"
)
const maxLogoSize = 2 * 1024 * 1024 // 2 MB
// ── Server extension fields and wiring ──────────────────────────────────────
// ldapStore and tenantStore are added to the Server struct via SetLDAP / SetTenants.
// They are declared separately from server.go to keep that file unmodified.
// Access is via the embedded pointer fields on *Server.
// SetLDAP wires the LDAP config store into the API server.
func (s *Server) SetLDAP(store *ldapcfg.Store) {
s.ldapStore = store
// Register LDAP routes only after the store is available.
s.mux.HandleFunc("GET /api/admin/ldap", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleGetLDAP)))
s.mux.HandleFunc("PUT /api/admin/ldap", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleSaveLDAP)))
s.mux.HandleFunc("DELETE /api/admin/ldap", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleDeleteLDAP)))
s.mux.HandleFunc("POST /api/admin/ldap/test", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleTestLDAP)))
}
// SetTenants wires the tenant store into the API server.
func (s *Server) SetTenants(store *tenantstore.Store) {
s.tenantStore = store
// Register tenant routes only after the store is available.
s.mux.HandleFunc("GET /api/tenants", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleListTenants)))
s.mux.HandleFunc("POST /api/tenants", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleCreateTenant)))
s.mux.HandleFunc("GET /api/tenants/{id}", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleGetTenant)))
s.mux.HandleFunc("PATCH /api/tenants/{id}", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleUpdateTenant)))
s.mux.HandleFunc("DELETE /api/tenants/{id}", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleDeleteTenant)))
s.mux.HandleFunc("GET /api/tenants/{id}/domains", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleListTenantDomains)))
s.mux.HandleFunc("POST /api/tenants/{id}/domains", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleAddTenantDomain)))
s.mux.HandleFunc("DELETE /api/tenants/{id}/domains/{did}", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleRemoveTenantDomain)))
s.mux.HandleFunc("GET /api/tenants/{id}/users", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleListTenantUsers)))
// Logo routes: read is tenant-scoped inside handleGetTenantLogo (PROJ-61); admin can write
s.mux.HandleFunc("GET /api/tenants/{id}/logo", s.auth(s.handleGetTenantLogo))
s.mux.HandleFunc("POST /api/tenants/{id}/logo", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleUploadTenantLogo)))
s.mux.HandleFunc("DELETE /api/tenants/{id}/logo", s.authMiddleware(s.requireRole(userstore.RoleAdmin, s.handleDeleteTenantLogo)))
// Logo routes for domain_admin (own tenant)
s.mux.HandleFunc("GET /api/tenant/logo", s.authAdmin(s.handleGetOwnTenantLogo))
s.mux.HandleFunc("POST /api/tenant/logo", s.authAdmin(s.handleUploadOwnTenantLogo))
s.mux.HandleFunc("DELETE /api/tenant/logo", s.authAdmin(s.handleDeleteOwnTenantLogo))
}
// SetTenantLDAP wires the per-tenant LDAP config store into the API server and
// registers the tenant LDAP routes.
func (s *Server) SetTenantLDAP(store *ldapcfg.TenantStore) {
s.tenantLdapStore = store
// domain_admin routes — tenant_id comes from JWT session, NOT from URL
s.mux.HandleFunc("GET /api/tenant/ldap", s.authAdmin(s.handleGetTenantLDAP))
s.mux.HandleFunc("PUT /api/tenant/ldap", s.authAdmin(s.handleSaveTenantLDAP))
s.mux.HandleFunc("DELETE /api/tenant/ldap", s.authAdmin(s.handleDeleteTenantLDAP))
s.mux.HandleFunc("POST /api/tenant/ldap/test", s.authAdmin(s.handleTestTenantLDAP))
s.mux.HandleFunc("POST /api/tenant/ldap/sync", s.authAdmin(s.handleSyncTenantLDAP))
// superadmin routes — tenant_id from URL parameter
s.mux.HandleFunc("GET /api/admin/tenants/{id}/ldap", s.authMiddleware(s.requireRole(userstore.RoleSuperAdmin, s.handleAdminGetTenantLDAP)))
s.mux.HandleFunc("PUT /api/admin/tenants/{id}/ldap", s.authMiddleware(s.requireRole(userstore.RoleSuperAdmin, s.handleAdminSaveTenantLDAP)))
s.mux.HandleFunc("DELETE /api/admin/tenants/{id}/ldap", s.authMiddleware(s.requireRole(userstore.RoleSuperAdmin, s.handleAdminDeleteTenantLDAP)))
s.mux.HandleFunc("POST /api/admin/tenants/{id}/ldap/test", s.authMiddleware(s.requireRole(userstore.RoleSuperAdmin, s.handleAdminTestTenantLDAP)))
s.mux.HandleFunc("POST /api/admin/tenants/{id}/ldap/sync", s.authMiddleware(s.requireRole(userstore.RoleSuperAdmin, s.handleAdminSyncTenantLDAP)))
}