From fa9f77782c37d11cfc70fbde651b562c0b349ce1 Mon Sep 17 00:00:00 2001 From: sysops Date: Thu, 28 May 2026 23:55:31 +0200 Subject: [PATCH] =?UTF-8?q?fix(sec):=20Authorization-Byp=C3=A4sse=20und=20?= =?UTF-8?q?Path-Traversal=20schlie=C3=9Fen,=20Xapian-Doku=20bereinigen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SEC: requireMailAccess auf GET /api/threads/{threadID} — superadmin/domain_admin konnten Mail-Metadaten lesen - SEC: requireMailAccess auf POST /api/export/ediscovery — superadmin/domain_admin konnten bis zu 10k EML exportieren - SEC: V1-API user-role Keys müssen 'contact=' angeben — verhindert vollständige Tenant-Enumeration - SEC: Domain-Regex-Validierung in handleCertACME vor filepath.Join und certbot-Aufruf - docs: README und config.test.yml auf Manticore Search aktualisiert (kein Xapian mehr) Co-Authored-By: Claude Sonnet 4.6 --- README.md | 48 ++++++++++++++++------------------- config.test.yml | 4 +-- internal/api/cert_handlers.go | 7 +++++ internal/api/server.go | 4 +-- internal/api/v1_handlers.go | 13 +++++----- 5 files changed, 39 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index ba4ab52..e5c5219 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Ein selbst gehostetes, unternehmenstaugliches Mail-Archiv-System. Empfängt E-Ma | IMAP-Import manuell & automatischer Sync | ✅ Deployed | | EML/MBOX Web-Upload (Admin) | ✅ Deployed | | AES-256-GCM verschlüsselte Speicherung | ✅ Deployed | -| Xapian Volltext-Indexierung (async) | ✅ Deployed | +| Manticore Search Volltext-Indexierung (async) | ✅ Deployed | | Volltext-Suche mit Filtern & Sortierung | ✅ Deployed | | E-Mail-Ansicht mit HTML-Sandbox | ✅ Deployed | | E-Mail-Export als EML / PDF / ZIP | ✅ Deployed | @@ -71,8 +71,8 @@ Ein selbst gehostetes, unternehmenstaugliches Mail-Archiv-System. Empfängt E-Ma │ ▼ │──► /var/archivmail/store │ Async Index Worker │ (AES-256-GCM) │ │ │ - │ ▼ │──► /var/archivmail/xapian - │ Xapian Index │ (Volltext-Index) + │ ▼ │──► Manticore Search (Port 9306) + │ Manticore Index │ (Volltext-Index) └─────────────────────────────────────┘ ▲ │ /api/* @@ -87,10 +87,10 @@ Ein selbst gehostetes, unternehmenstaugliches Mail-Archiv-System. Empfängt E-Ma | Komponente | Technologie | Beschreibung | |------------|-------------|--------------| -| Backend | Go 1.23 | REST API, SMTP-Daemon, IMAP-Importer, Storage, Indexierung | +| Backend | Go 1.24 | REST API, SMTP-Daemon, IMAP-Importer, Storage, Indexierung | | Frontend | Next.js 16 (TypeScript) | Web-Oberfläche, Tailwind CSS, shadcn/ui | | Datenbank | PostgreSQL | Metadaten, Benutzer, Audit-Log, Session-Blacklist | -| Volltextsuche | Xapian | BM25-Ranking, Wildcards, Feldpräfixe | +| Volltextsuche | Manticore Search | BM25-Ranking, Wildcards, Feldpräfixe | | Speicherung | Dateisystem | AES-256-GCM verschlüsselt, SHA-256 Integrität | --- @@ -99,10 +99,10 @@ Ein selbst gehostetes, unternehmenstaugliches Mail-Archiv-System. Empfängt E-Ma **Server:** - Debian 12 / Ubuntu 22.04 oder neuer -- Go ≥ 1.23 (mit CGO für Xapian) +- Go ≥ 1.24 (CGO_ENABLED=0, keine C-Abhängigkeiten) - Node.js ≥ 20 - PostgreSQL ≥ 14 -- libxapian-dev +- Manticore Search ≥ 6.x **Ports (Standard):** - `8080` – HTTP API (Backend) @@ -121,9 +121,9 @@ curl -fsSL https://gitea.perlbach24.de/scripte/archivmail/raw/branch/main/update Das Skript: 1. Klont/aktualisiert den Quellcode -2. Baut Backend (Go + CGO/Xapian) und Frontend (Next.js) +2. Baut Backend (Go, CGO_ENABLED=0) und Frontend (Next.js) 3. Stoppt Dienste, spielt Binaries ein, startet Dienste neu -4. Führt Datenbankmigrationen durch +4. Führt Datenbankmigrationen durch und reindexiert Manticore ### Manuell @@ -133,7 +133,7 @@ git clone https://gitea.perlbach24.de/scripte/archivmail.git /opt/archivmail/_bu cd /opt/archivmail/_build # Backend bauen -CGO_ENABLED=1 go build -tags xapian -buildvcs=false \ +CGO_ENABLED=0 go build -buildvcs=false \ -o /opt/archivmail/bin/archivmail ./cmd/archivmail/ # Frontend bauen @@ -145,7 +145,7 @@ rsync -a --delete .next/static/ /opt/archivmail/web/.next/static/ ### Konfigurationsdatei anlegen ```bash -mkdir -p /etc/archivmail /var/archivmail/{store,astore,xapian} /var/log/archivmail +mkdir -p /etc/archivmail /var/archivmail/{store,astore} /var/log/archivmail cat > /etc/archivmail/config.yml << 'EOF' server: @@ -163,7 +163,6 @@ database: storage: store_path: /var/archivmail/store astore_path: /var/archivmail/astore - xapian_path: /var/archivmail/xapian keyfile: /etc/archivmail/keyfile audit: @@ -183,10 +182,9 @@ api: secret: ZUFAELLIGER_JWT_SECRET_MINDESTENS_32_ZEICHEN index: - path: /var/archivmail/xapian - backend: xapian + backend: manticore + manticore_dsn: "manticore@tcp(127.0.0.1:9306)/" batch_size: 100 - async_queue_size: 1000 EOF # AES-256 Schlüssel generieren @@ -216,7 +214,6 @@ database: storage: store_path: /var/archivmail/store # Haupt-Mailspeicher (AES-256-GCM) astore_path: /var/archivmail/astore # Anhang-Speicher - xapian_path: /var/archivmail/xapian # Xapian-Datenbankpfad (auch in index.path) keyfile: /etc/archivmail/keyfile # 32-Byte AES-Schlüsseldatei smtp: @@ -234,10 +231,9 @@ api: secret: "" # JWT-Signaturschlüssel (mind. 32 Zeichen, zufällig) index: - path: /var/archivmail/xapian - backend: xapian # xapian (einziger unterstützter Backend) - batch_size: 100 # Dokumente pro Index-Batch - async_queue_size: 1000 # Größe der asynchronen Index-Queue + backend: manticore # manticore (Standard) + manticore_dsn: "manticore@tcp(127.0.0.1:9306)/" # Manticore Search DSN + batch_size: 100 # Dokumente pro Index-Batch audit: log_path: /var/log/archivmail/audit.log @@ -451,7 +447,7 @@ Alle E-Mails werden verschlüsselt auf dem Dateisystem gespeichert. ### Volltext-Suche -Suche über alle archivierten E-Mails per Xapian-Index unter `/search`. +Suche über alle archivierten E-Mails per Manticore Search unter `/search`. **Suchfelder:** - Freitext (durchsucht Betreff, Body, Absender, Empfänger, Anhangsnamen) @@ -461,13 +457,13 @@ Suche über alle archivierten E-Mails per Xapian-Index unter `/search`. - Nur Mails mit Anhängen (`has_attachment`) - Sortierung: `date_desc` (Standard), `date_asc`, `relevance` -**Xapian-Suchsyntax:** +**Suchsyntax:** ``` -Rechnung AND 2024 # UND-Verknüpfung +Rechnung 2024 # Mehrere Begriffe (AND) "Angebot Projekt X" # Phrase Rechn* # Wildcard -from:chef@firma.de # Feldpräfix -subject:Urlaubsantrag # Feldpräfix +@from chef@firma.de # Feldsuche Absender +@subject Urlaubsantrag # Feldsuche Betreff ``` **Paginierung:** 25 Ergebnisse pro Seite (konfigurierbar via `page_size`) @@ -827,7 +823,7 @@ curl -fsSL https://gitea.perlbach24.de/scripte/archivmail/raw/branch/main/update Das Update-Skript führt automatisch durch: 1. Quellcode aktualisieren (git pull) -2. Backend neu bauen (CGO + Xapian) +2. Backend neu bauen (CGO_ENABLED=0) 3. Frontend neu bauen (Next.js standalone) 4. Dienste stoppen 5. Binaries und Frontend einspielen diff --git a/config.test.yml b/config.test.yml index f838fe8..a5a0386 100644 --- a/config.test.yml +++ b/config.test.yml @@ -37,8 +37,8 @@ api: tls: false index: - path: /tmp/archivmail-test/xapian - backend: xapian + backend: manticore + manticore_dsn: "manticore@tcp(127.0.0.1:9306)/" batch_size: 10 logging: diff --git a/internal/api/cert_handlers.go b/internal/api/cert_handlers.go index 7daa30f..1f7d628 100644 --- a/internal/api/cert_handlers.go +++ b/internal/api/cert_handlers.go @@ -17,10 +17,13 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strings" "time" ) +var validDomainRE = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$`) + const ( certDir = "/etc/ssl/archivmail" certPath = "/etc/ssl/archivmail/archivmail.crt" @@ -270,6 +273,10 @@ func (s *Server) handleCertACME(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, "email is required") return } + if !validDomainRE.MatchString(req.Domain) { + writeError(w, http.StatusBadRequest, "invalid domain name") + return + } // Verify certbot is available before attempting anything. certbotPath, err := exec.LookPath("certbot") diff --git a/internal/api/server.go b/internal/api/server.go index 8eae884..7aabe46 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -213,7 +213,7 @@ func (s *Server) routes() { s.mux.HandleFunc("GET /api/admin/storage/stats", s.authAdmin(s.handleStorageStats)) s.mux.HandleFunc("GET /api/mails/{id}", s.auth(s.requireMailAccess(s.handleGetMail))) s.mux.HandleFunc("GET /api/mails/{id}/attachments/{index}", s.auth(s.requireMailAccess(s.handleGetAttachment))) - s.mux.HandleFunc("GET /api/threads/{threadID}", s.auth(s.handleGetThread)) + s.mux.HandleFunc("GET /api/threads/{threadID}", s.auth(s.requireMailAccess(s.handleGetThread))) s.mux.HandleFunc("GET /api/mails/{id}/raw", s.auth(s.requireMailAccess(s.handleGetRaw))) // PROJ-44: OCR-Text-Download — gleicher ACL-Pfad wie /raw. s.mux.HandleFunc("GET /api/mails/{id}/ocr-text", s.auth(s.requireMailAccess(s.handleGetOCRText))) @@ -256,7 +256,7 @@ func (s *Server) routes() { // Export routes s.mux.HandleFunc("GET /api/export/pdf/{id}", s.auth(s.requireMailAccess(s.handleExportPDF))) s.mux.HandleFunc("POST /api/export/zip", s.auth(s.requireMailAccess(s.handleExportZIP))) - s.mux.HandleFunc("POST /api/export/ediscovery", s.auth(s.handleExportEDiscovery)) + s.mux.HandleFunc("POST /api/export/ediscovery", s.auth(s.requireMailAccess(s.handleExportEDiscovery))) // Upload routes (admin only) s.mux.HandleFunc("POST /api/admin/upload", s.authAdmin(s.handleUpload)) diff --git a/internal/api/v1_handlers.go b/internal/api/v1_handlers.go index be7a0fb..ca46028 100644 --- a/internal/api/v1_handlers.go +++ b/internal/api/v1_handlers.go @@ -64,6 +64,12 @@ func (s *Server) handleV1SearchMails(w http.ResponseWriter, r *http.Request) { Page: page, } + // User-role keys must always scope their search to a specific contact address. + if akSess.Role == "user" && contactFilter == "" { + writeError(w, http.StatusBadRequest, "user-role API keys require the 'contact' parameter") + return + } + // "contact" searches both From and To fields via OwnEmail. if contactFilter != "" { req.OwnEmail = contactFilter @@ -153,13 +159,6 @@ func (s *Server) handleV1SearchMails(w http.ResponseWriter, r *http.Request) { } m.HasAttachments = len(pm.Attachments) > 0 - // Role-based filtering: "user" role only sees mails they are involved in. - if akSess.Role == "user" { - // User keys need a contact filter or the mail must belong to the tenant. - // For user-role keys without explicit contact filter, we still return - // all tenant mails (tenant isolation is handled by the index). - } - mails = append(mails, m) }