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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -171,11 +172,11 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
labelMap, _ = s.labels.GetLabelsForEmails(r.Context(), emailIDs)
|
||||
}
|
||||
|
||||
// SEC: For user role, restrict results to mails the user is involved in
|
||||
// (From, To, or CC). Email comes from the JWT session — no DB lookup needed.
|
||||
// If email is missing for a user-role session, block all results (fail-safe).
|
||||
// SEC: For user and auditor roles, restrict results to mails the user is
|
||||
// involved in (From, To, or CC). Email comes from the JWT session — no DB
|
||||
// lookup needed. If email is missing, block all results (fail-safe).
|
||||
var userEmailFilter string
|
||||
if sess.Role == userstore.RoleUser {
|
||||
if sess.Role == userstore.RoleUser || sess.Role == userstore.RoleAuditor {
|
||||
userEmailFilter = strings.ToLower(sess.Email)
|
||||
if userEmailFilter == "" {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Users can access mails they sent as well as mails they received.
|
||||
// From may contain a display name ("Name <addr>"), so Contains is used.
|
||||
// Addresses are parsed with net/mail.ParseAddressList so that display names
|
||||
// ("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 {
|
||||
email := strings.ToLower(userEmail)
|
||||
if strings.Contains(strings.ToLower(pm.From), email) {
|
||||
target := strings.ToLower(userEmail)
|
||||
|
||||
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
|
||||
}
|
||||
for _, to := range pm.To {
|
||||
if strings.Contains(strings.ToLower(to), email) {
|
||||
if checkHeader(to) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, cc := range pm.CC {
|
||||
if strings.Contains(strings.ToLower(cc), email) {
|
||||
if checkHeader(cc) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -117,6 +118,11 @@ func (m *Manager) Login(username, password string) (token string, user *userstor
|
||||
}
|
||||
}
|
||||
email := attrs["mail"]
|
||||
if email != "" {
|
||||
if _, err := mail.ParseAddress(email); err != nil {
|
||||
email = "" // invalid mail attribute — fall back
|
||||
}
|
||||
}
|
||||
if email == "" {
|
||||
email = username
|
||||
}
|
||||
@@ -163,6 +169,11 @@ func (m *Manager) Login(username, password string) (token string, user *userstor
|
||||
}
|
||||
}
|
||||
email := attrs["mail"]
|
||||
if email != "" {
|
||||
if _, err := mail.ParseAddress(email); err != nil {
|
||||
email = "" // invalid mail attribute — fall back
|
||||
}
|
||||
}
|
||||
if email == "" {
|
||||
email = username + "@ldap.local"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user