fix(security): Email-Matching, LDAP-Validierung, Auditor-Isolation
- mailBelongsToUser: net/mail.ParseAddressList statt strings.Contains verhindert False-Positives durch Display-Namen in Mail-Headern - LDAP mail-Attribut: net/mail.ParseAddress-Validierung vor Übernahme, Fallback auf username / username@ldap.local bei ungültiger Adresse - handleSearch: Auditor-Rolle in userEmailFilter-Check eingeschlossen, sodass Auditoren im Search-Pfad dieselbe Mail-Isolation erhalten wie User Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/mail"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -171,11 +172,11 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
|
|||||||
labelMap, _ = s.labels.GetLabelsForEmails(r.Context(), emailIDs)
|
labelMap, _ = s.labels.GetLabelsForEmails(r.Context(), emailIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SEC: For user role, restrict results to mails the user is involved in
|
// SEC: For user and auditor roles, restrict results to mails the user is
|
||||||
// (From, To, or CC). Email comes from the JWT session — no DB lookup needed.
|
// involved in (From, To, or CC). Email comes from the JWT session — no DB
|
||||||
// If email is missing for a user-role session, block all results (fail-safe).
|
// lookup needed. If email is missing, block all results (fail-safe).
|
||||||
var userEmailFilter string
|
var userEmailFilter string
|
||||||
if sess.Role == userstore.RoleUser {
|
if sess.Role == userstore.RoleUser || sess.Role == userstore.RoleAuditor {
|
||||||
userEmailFilter = strings.ToLower(sess.Email)
|
userEmailFilter = strings.ToLower(sess.Email)
|
||||||
if userEmailFilter == "" {
|
if userEmailFilter == "" {
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{"total": 0, "hits": []interface{}{}})
|
writeJSON(w, http.StatusOK, map[string]interface{}{"total": 0, "hits": []interface{}{}})
|
||||||
@@ -418,21 +419,55 @@ func (s *Server) handleGetRaw(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(raw)
|
w.Write(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// emailsFromHeader parses a mail header value (e.g. From, To, CC) and returns
|
||||||
|
// the bare email addresses it contains. Parsing uses net/mail.ParseAddressList
|
||||||
|
// for correctness; if parsing fails the raw value is returned as a single
|
||||||
|
// lower-cased string so that the caller can still apply a substring fallback.
|
||||||
|
func emailsFromHeader(header string) []string {
|
||||||
|
addrs, err := mail.ParseAddressList(header)
|
||||||
|
if err != nil {
|
||||||
|
// Parsing failed (e.g. malformed header) — return raw value for fallback.
|
||||||
|
return []string{strings.ToLower(header)}
|
||||||
|
}
|
||||||
|
out := make([]string, len(addrs))
|
||||||
|
for i, a := range addrs {
|
||||||
|
out[i] = strings.ToLower(a.Address)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// mailBelongsToUser checks if the user's email appears in From, To, or CC.
|
// mailBelongsToUser checks if the user's email appears in From, To, or CC.
|
||||||
// Users can access mails they sent as well as mails they received.
|
// Addresses are parsed with net/mail.ParseAddressList so that display names
|
||||||
// From may contain a display name ("Name <addr>"), so Contains is used.
|
// ("Name <addr>") do not cause false positives or negatives.
|
||||||
|
// Falls back to case-insensitive substring matching when parsing fails.
|
||||||
func mailBelongsToUser(pm *mailparser.ParsedMail, userEmail string) bool {
|
func mailBelongsToUser(pm *mailparser.ParsedMail, userEmail string) bool {
|
||||||
email := strings.ToLower(userEmail)
|
target := strings.ToLower(userEmail)
|
||||||
if strings.Contains(strings.ToLower(pm.From), email) {
|
|
||||||
|
checkHeader := func(header string) bool {
|
||||||
|
parsed := emailsFromHeader(header)
|
||||||
|
for _, addr := range parsed {
|
||||||
|
// Exact match after parsing — preferred path.
|
||||||
|
if addr == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Fallback: if parsing returned the raw value (error path), use substring.
|
||||||
|
if strings.Contains(addr, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkHeader(pm.From) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for _, to := range pm.To {
|
for _, to := range pm.To {
|
||||||
if strings.Contains(strings.ToLower(to), email) {
|
if checkHeader(to) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, cc := range pm.CC {
|
for _, cc := range pm.CC {
|
||||||
if strings.Contains(strings.ToLower(cc), email) {
|
if checkHeader(cc) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -117,6 +118,11 @@ func (m *Manager) Login(username, password string) (token string, user *userstor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
email := attrs["mail"]
|
email := attrs["mail"]
|
||||||
|
if email != "" {
|
||||||
|
if _, err := mail.ParseAddress(email); err != nil {
|
||||||
|
email = "" // invalid mail attribute — fall back
|
||||||
|
}
|
||||||
|
}
|
||||||
if email == "" {
|
if email == "" {
|
||||||
email = username
|
email = username
|
||||||
}
|
}
|
||||||
@@ -163,6 +169,11 @@ func (m *Manager) Login(username, password string) (token string, user *userstor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
email := attrs["mail"]
|
email := attrs["mail"]
|
||||||
|
if email != "" {
|
||||||
|
if _, err := mail.ParseAddress(email); err != nil {
|
||||||
|
email = "" // invalid mail attribute — fall back
|
||||||
|
}
|
||||||
|
}
|
||||||
if email == "" {
|
if email == "" {
|
||||||
email = username + "@ldap.local"
|
email = username + "@ldap.local"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user