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:
+29
-5
@@ -3,9 +3,11 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -14,6 +16,8 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
|
||||
"github.com/archivmail/config"
|
||||
"github.com/archivmail/internal/api"
|
||||
"github.com/archivmail/internal/audit"
|
||||
@@ -67,6 +71,26 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// SEC-08: Derive separate keys from the master secret to prevent key reuse.
|
||||
// jwtSecret is used for JWT token signing only.
|
||||
// aesKey is used for AES-256-GCM encryption of stored passwords (IMAP, POP3, LDAP).
|
||||
// HKDF is deterministic: same cfg.API.Secret always produces the same derived keys.
|
||||
// NOTE: After this change, existing stored IMAP/POP3/LDAP passwords must be
|
||||
// re-entered once, as they were encrypted with the old undivided key.
|
||||
masterKey := []byte(cfg.API.Secret)
|
||||
jwtKeyRaw := make([]byte, 32)
|
||||
if _, err := io.ReadFull(hkdf.New(sha256.New, masterKey, []byte("archivmail-jwt-v1"), nil), jwtKeyRaw); err != nil {
|
||||
logger.Error("key derivation failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
aesKeyRaw := make([]byte, 32)
|
||||
if _, err := io.ReadFull(hkdf.New(sha256.New, masterKey, []byte("archivmail-aes-v1"), nil), aesKeyRaw); err != nil {
|
||||
logger.Error("key derivation failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
jwtSecret := hex.EncodeToString(jwtKeyRaw)
|
||||
aesKey := hex.EncodeToString(aesKeyRaw)
|
||||
|
||||
// Storage with encryption + DB metadata
|
||||
storeCfg := storage.Config{
|
||||
Dir: cfg.Storage.StorePath,
|
||||
@@ -127,7 +151,7 @@ func main() {
|
||||
}
|
||||
|
||||
// LDAP config store
|
||||
ldapSt, err := ldapcfg.New(cfg.Database.DSN(), cfg.API.Secret)
|
||||
ldapSt, err := ldapcfg.New(cfg.Database.DSN(), aesKey)
|
||||
if err != nil {
|
||||
logger.Error("ldap config store init failed", "err", err)
|
||||
os.Exit(1)
|
||||
@@ -135,12 +159,12 @@ func main() {
|
||||
defer ldapSt.Close()
|
||||
|
||||
// Auth manager (with LDAP fallback)
|
||||
authMgr := auth.New(users, ldapSt, cfg.API.Secret)
|
||||
authMgr := auth.New(users, ldapSt, jwtSecret)
|
||||
|
||||
// API server
|
||||
apiCfg := config.APIConfig{
|
||||
Bind: cfg.API.Bind,
|
||||
Secret: cfg.API.Secret,
|
||||
Secret: jwtSecret,
|
||||
}
|
||||
srv := api.New(apiCfg, mailStore, idx, authMgr, users, audlog, logger)
|
||||
|
||||
@@ -200,7 +224,7 @@ func main() {
|
||||
srv.SetSMTPDaemon(smtpDaemon)
|
||||
|
||||
// IMAP store + importer + scheduler (wired to use async worker)
|
||||
imapSt, err := imapstore.New(cfg.Database.DSN(), cfg.API.Secret)
|
||||
imapSt, err := imapstore.New(cfg.Database.DSN(), aesKey)
|
||||
if err != nil {
|
||||
logger.Error("imap store init failed", "err", err)
|
||||
os.Exit(1)
|
||||
@@ -213,7 +237,7 @@ func main() {
|
||||
srv.SetImap(imapSt, imapImp, imapSched)
|
||||
|
||||
// POP3 store + importer
|
||||
pop3St, err := pop3store.New(cfg.Database.DSN(), cfg.API.Secret)
|
||||
pop3St, err := pop3store.New(cfg.Database.DSN(), aesKey)
|
||||
if err != nil {
|
||||
logger.Error("pop3 store init failed", "err", err)
|
||||
os.Exit(1)
|
||||
|
||||
Reference in New Issue
Block a user