Overview
CVE-2026-33665 describes an account takeover in n8n where LDAP-authenticated users were automatically linked to an existing local account if the LDAP email attribute matched a local account's email. An attacker who could modify their LDAP email attribute to match another user’s email (including an administrator) could log in and gain full access to that account. The linkage persisted even if the LDAP email later changed, resulting in a permanent takeover. The issue was fixed in n8n versions 2.4.0 and 1.121.0. In Go (Gin) applications, a similar class of broken authentication can occur if the login flow blindly maps an external LDAP identity to an internal user account based solely on email matches (CWE-287). A real-world remediation requires avoiding automatic linking by emails and instead using stable, explicit identity bindings (e.g., LDAP IDs) with proper admin/user consent for linking. The CVE reference highlights the risk of relying on mutable attributes like email and the importance of binding external identities to internal accounts with a robust, auditable mapping.
In Go (Gin) contexts, reproduce this risk by implementing a login path that, upon successful LDAP authentication, cross-links the LDAP identity to a local user by email alone. If an attacker can alter the LDAP email value to match another local user, they could gain that user’s access. Remediations include: stop auto-linking by email, bind identities by a stable LDAP identifier (not the email), and require explicit linking flows with proper authorization and auditing. The vulnerability aligns with CWE-287, and real-world mitigations emphasize disallowing unchecked automatic mappings and limiting attack surfaces when external identities are involved. The guidance also notes that the original vendor fixed the issue at specific versions and that temporary mitigations (e.g., disable LDAP login, restrict email attribute changes) are not sufficient long-term fixes. [Fixed March 2026]
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"errors"
"fmt"
)
type User struct {
ID int
Email string
LDAPID string // stable LDAP identifier
}
type Store struct {
users map[int]*User
emails map[string]int // email -> userID
ldapIndex map[string]int // ldapID -> userID
nextID int
}
func NewStore() *Store {
return &Store{users: make(map[int]*User), emails: make(map[string]int), ldapIndex: make(map[string]int), nextID: 1}
}
func (s *Store) SaveUser(u *User) *User {
if u.ID == 0 {
u.ID = s.nextID
s.nextID++
}
s.users[u.ID] = u
if u.Email != "" {
s.emails[u.Email] = u.ID
}
if u.LDAPID != "" {
s.ldapIndex[u.LDAPID] = u.ID
}
return u
}
func (s *Store) GetUserByEmail(email string) *User {
if id, ok := s.emails[email]; ok {
return s.users[id]
}
return nil
}
func (s *Store) GetUserByLDAPID(ldapID string) *User {
if id, ok := s.ldapIndex[ldapID]; ok {
return s.users[id]
}
return nil
}
// Vulnerable: auto-link LDAP identity to any local user with the same email
func vulnerableLogin(store *Store, ldapID, ldapEmail string) *User {
if u := store.GetUserByEmail(ldapEmail); u != nil {
// Auto-link LDAP to existing user by email
u.LDAPID = ldapID
store.ldapIndex[ldapID] = u.ID
return u
}
// Create new user account tied to LDAP
u := &User{Email: ldapEmail, LDAPID: ldapID}
store.SaveUser(u)
return u
}
// Fixed: do not auto-link by email. use explicit mapping via LDAPID only.
func fixedLogin(store *Store, ldapID, ldapEmail string) (*User, error) {
// If LDAP identity is already linked, login that user
if u := store.GetUserByLDAPID(ldapID); u != nil {
return u, nil
}
// Do not map to an existing local user by email automatically
if u := store.GetUserByEmail(ldapEmail); u != nil {
return nil, errors.New("LDAP identity email already belongs to a local user; linking requires explicit admin consent.")
}
// Create a new user account for this LDAP identity
u := &User{Email: ldapEmail, LDAPID: ldapID}
store.SaveUser(u)
return u, nil
}
func main() {
// Demonstration of vulnerability
store := NewStore()
admin := &User{Email: "[email protected]"}
store.SaveUser(admin)
fmt.Println("VULNERABLE scenario:")
v := vulnerableLogin(store, "LDAP-UID-ATTACK", "[email protected]")
fmt.Printf("Logged in user: ID=%d, Email=%s, LDAPID=%s\n", v.ID, v.Email, v.LDAPID)
// Demonstration of fix (isolated from previous store)
store2 := NewStore()
admin2 := &User{Email: "[email protected]"}
store2.SaveUser(admin2)
fmt.Println("FIXED scenario:")
if u, err := fixedLogin(store2, "LDAP-UID-ATTACK", "[email protected]"); err != nil {
fmt.Println("Login blocked:", err)
} else {
fmt.Printf("Logged in user: ID=%d, Email=%s, LDAPID=%s\n", u.ID, u.Email, u.LDAPID)
}
}