fix(security): Fail-closed Tenant-Filter in v1-Suche + SMTP-Out-Key-Kette gehärtet
Security-Audit-Nachtrag (siehe PROJ-64): - internal/api/v1_handlers.go: handleV1SearchMails fehlte der fail-closed Tenant-Post-Filter-Fallback für den Fall idxMgr==nil (gleiches Muster wie bereits in search_handlers.go). Aktuell nicht ausnutzbar, da idxMgr in main.go immer gesetzt wird, aber strukturelle Absicherung gegen künftige Regressionen (analog PROJ-55 BUG-1). - internal/smtpoutconfig/store.go: Verschlüsselungsschlüssel wird jetzt aus dem HKDF-abgeleiteten aesKey gebildet statt aus dem rohen cfg.API.Secret, konsistent mit internal/ldapconfig und internal/imap/store.go (SEC-08). Verifiziert: smtp_out_config auf Produktiv (131) war leer, kein Breaking Change für bestehend gespeicherte Zugangsdaten. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
be93614c9f
commit
99e4c1bbe4
@@ -115,8 +115,10 @@ func (s *Server) handleV1SearchMails(w http.ResponseWriter, r *http.Request) {
|
||||
// Resolve per-tenant index.
|
||||
tenantID := akSess.TenantID
|
||||
searchIdx := s.idx
|
||||
usedTenantIndex := false
|
||||
if s.idxMgr != nil && tenantID != 0 {
|
||||
searchIdx = s.idxMgr.ForTenant(&tenantID)
|
||||
usedTenantIndex = true
|
||||
}
|
||||
|
||||
result, err := searchIdx.Search(req)
|
||||
@@ -126,6 +128,26 @@ func (s *Server) handleV1SearchMails(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// PROJ-64 (Security-Nachtrag): fail-closed tenant post-filter for the legacy
|
||||
// path where idxMgr is nil, mirroring the fallback in search_handlers.go.
|
||||
if tenantID != 0 && !usedTenantIndex && len(result.Hits) > 0 {
|
||||
allowedIDs, idErr := s.store.GetAllIDsByTenant(r.Context(), &tenantID)
|
||||
if idErr == nil {
|
||||
allowed := make(map[string]struct{}, len(allowedIDs))
|
||||
for _, id := range allowedIDs {
|
||||
allowed[id] = struct{}{}
|
||||
}
|
||||
filtered := result.Hits[:0]
|
||||
for _, h := range result.Hits {
|
||||
if _, ok := allowed[h.ID]; ok {
|
||||
filtered = append(filtered, h)
|
||||
}
|
||||
}
|
||||
result.Hits = filtered
|
||||
result.Total = len(filtered)
|
||||
}
|
||||
}
|
||||
|
||||
// Audit log.
|
||||
s.audlog.Log(audit.Entry{
|
||||
EventType: audit.EventSearch,
|
||||
|
||||
@@ -53,6 +53,10 @@ type Store struct {
|
||||
}
|
||||
|
||||
// New connects to PostgreSQL, creates the table if needed, and returns a Store.
|
||||
// secret is the HKDF-derived aesKey (see main.go, SEC-08) — not the raw
|
||||
// cfg.API.Secret — so this store shares the same key-derivation lineage as
|
||||
// internal/ldapconfig and internal/imap/store.go. The "-smtpout" suffix keeps
|
||||
// this store's encryption key domain-separated from those.
|
||||
func New(dsn, secret string) (*Store, error) {
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, dsn)
|
||||
|
||||
Reference in New Issue
Block a user