fix(security): Kritische Sicherheitslücken beheben (SEC-01/02/03/05/08/17/22/26/28)
- SEC-01: Privilege Escalation verhindert — Rollenhierarchie in Create/Update/DeleteUser
- SEC-02: Tenant-Isolation in Update/DeleteUser — domain_admin nur eigene Nutzer
- SEC-03: IMAP/POP3 Owner-Check via auth.HasRole statt direktem String-Vergleich
- SEC-05: Export PDF/ZIP prüft Tenant-Zugehörigkeit vor Dateiausgabe
- SEC-08: HKDF-SHA256 trennt JWT-Secret von AES-Key (archivmail-jwt-v1 / archivmail-aes-v1)
- SEC-17: handleSecurityFix erfordert requireRole(superadmin)
- SEC-22: Mail-ID Regex [0-9a-f]{64} in allen Handlern (Path-Traversal-Schutz)
- SEC-26: SMTP Fail-Closed — leere AllowedIPs blockiert alles statt zu erlauben
- SEC-28: handleGetRaw — Parse-Fehler bricht ab statt Fallthrough zu Dateizugriff
BREAKING: IMAP/POP3/LDAP-Passwörter müssen nach Deploy einmalig neu eingegeben
werden (neuer AES-Key). JWT-Sessions laufen ab (einmaliges Re-Login nötig).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+43
-1
@@ -327,6 +327,11 @@ func buildMailPDF(id string, pm *mailparser.ParsedMail, rawSize int) []byte {
|
||||
|
||||
func (s *Server) handleExportPDF(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
// SEC-22: Validate mail ID format to prevent path traversal.
|
||||
if !isValidMailID(id) {
|
||||
writeError(w, http.StatusBadRequest, "invalid mail id")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := s.store.Load(id)
|
||||
if err != nil {
|
||||
@@ -340,8 +345,18 @@ func (s *Server) handleExportPDF(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// User role: only own mailbox
|
||||
sess := sessionFromCtx(r.Context())
|
||||
|
||||
// SEC-05: Tenant isolation for PDF export.
|
||||
if sess.TenantID != nil {
|
||||
mailTenant, _ := s.store.GetTenantForMail(r.Context(), id)
|
||||
if mailTenant == nil || *mailTenant != *sess.TenantID {
|
||||
writeError(w, http.StatusForbidden, "access denied")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// User role: only own mailbox
|
||||
if sess.Role == userstore.RoleUser {
|
||||
u, err := s.users.GetByUsername(sess.Username)
|
||||
if err != nil || !mailBelongsToUser(pm, u.Email) {
|
||||
@@ -394,8 +409,35 @@ func (s *Server) handleExportZIP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// SEC-22: Validate all mail IDs before processing.
|
||||
for _, id := range req.IDs {
|
||||
if !isValidMailID(id) {
|
||||
writeError(w, http.StatusBadRequest, "invalid mail id")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sess := sessionFromCtx(r.Context())
|
||||
|
||||
// SEC-05: Tenant isolation — verify all requested IDs belong to caller's tenant.
|
||||
if sess.TenantID != nil {
|
||||
allowedIDs, err := s.store.GetAllIDsByTenant(r.Context(), sess.TenantID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "tenant check failed")
|
||||
return
|
||||
}
|
||||
allowed := make(map[string]struct{}, len(allowedIDs))
|
||||
for _, aid := range allowedIDs {
|
||||
allowed[aid] = struct{}{}
|
||||
}
|
||||
for _, id := range req.IDs {
|
||||
if _, ok := allowed[id]; !ok {
|
||||
writeError(w, http.StatusForbidden, "access denied")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For RoleUser, look up user email once for access checks
|
||||
var userEmail string
|
||||
if sess.Role == userstore.RoleUser {
|
||||
|
||||
Reference in New Issue
Block a user