fix: CVE-2026-33419 harden LDAP STS auth

Prevent username enumeration in AssumeRoleWithLDAPIdentity by returning the same external STS error for unknown users and invalid passwords, while preserving LDAP infrastructure failures as upstream errors so they continue to surface as 500s and remain visible in server logs.

Add a small in-memory rate limiter for LDAP STS login attempts, keyed by source IP and normalized username, and add regression coverage for auth failure classification, throttling, and Docker-backed LDAP end-to-end flows.

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Claude Code <claude-code@anthropic.com>
This commit is contained in:
Feng Ruohang
2026-04-15 13:49:51 +08:00
parent d24f449e08
commit 3b950f8fa8
5 changed files with 555 additions and 6 deletions
+42 -3
View File
@@ -30,6 +30,42 @@ import (
xldap "github.com/minio/pkg/v3/ldap"
)
var errAuthentication = errors.New("ldap authentication failed")
func isUserDNNotFoundError(err error) bool {
return err != nil && strings.Contains(strings.ToLower(err.Error()), "user dn not found for")
}
// authError marks authentication failures without changing the returned
// message, so callers can distinguish auth failures from upstream outages.
type authError struct {
err error
}
func (e authError) Error() string {
return e.err.Error()
}
func (e authError) Unwrap() error {
return e.err
}
func (e authError) Is(target error) bool {
return target == errAuthentication
}
func wrapAuthError(err error) error {
if err == nil {
return nil
}
return authError{err: err}
}
// IsAuthError reports whether err is an LDAP authentication failure.
func IsAuthError(err error) bool {
return errors.Is(err, errAuthentication)
}
// LookupUserDN searches for the full DN and groups of a given short/login
// username.
func (l *Config) LookupUserDN(username string) (*xldap.DNSearchResult, []string, error) {
@@ -86,7 +122,7 @@ func (l *Config) GetValidatedDNForUsername(username string) (*xldap.DNSearchResu
// the directory.
bindDN, err := l.LDAP.LookupUsername(conn, username)
if err != nil {
if strings.Contains(err.Error(), "User DN not found for") {
if isUserDNNotFoundError(err) {
return nil, nil
}
return nil, fmt.Errorf("Unable to find user DN: %w", err)
@@ -204,7 +240,7 @@ func (l *Config) GetValidatedDNWithGroups(username string) (*xldap.DNSearchResul
// the directory.
lookupRes, err = l.LDAP.LookupUsername(conn, username)
if err != nil {
if strings.Contains(err.Error(), "User DN not found for") {
if isUserDNNotFoundError(err) {
return nil, nil, nil
}
return nil, nil, fmt.Errorf("Unable to find user DN: %w", err)
@@ -245,6 +281,9 @@ func (l *Config) Bind(username, password string) (*xldap.DNSearchResult, []strin
lookupResult, err := l.LDAP.LookupUsername(conn, username)
if err != nil {
errRet := fmt.Errorf("Unable to find user DN: %w", err)
if isUserDNNotFoundError(err) {
return nil, nil, wrapAuthError(errRet)
}
return nil, nil, errRet
}
@@ -252,7 +291,7 @@ func (l *Config) Bind(username, password string) (*xldap.DNSearchResult, []strin
err = conn.Bind(lookupResult.ActualDN, password)
if err != nil {
errRet := fmt.Errorf("LDAP auth failed for DN %s: %w", lookupResult.ActualDN, err)
return nil, nil, errRet
return nil, nil, wrapAuthError(errRet)
}
// Bind to the lookup user account again to perform group search.
@@ -0,0 +1,45 @@
package ldap
import (
"errors"
"testing"
)
func TestWrapAuthError(t *testing.T) {
baseErr := errors.New("LDAP auth failed for DN uid=dillon,dc=min,dc=io")
err := wrapAuthError(baseErr)
if !IsAuthError(err) {
t.Fatal("expected wrapped error to be recognized as auth failure")
}
if err.Error() != baseErr.Error() {
t.Fatalf("expected error text %q, got %q", baseErr.Error(), err.Error())
}
}
func TestWrapAuthErrorNil(t *testing.T) {
if wrapAuthError(nil) != nil {
t.Fatal("expected nil input to stay nil")
}
}
func TestIsAuthErrorNegative(t *testing.T) {
if IsAuthError(errors.New("ldap unavailable")) {
t.Fatal("expected plain errors to not be classified as auth failures")
}
}
func TestIsUserDNNotFoundError(t *testing.T) {
if isUserDNNotFoundError(nil) {
t.Fatal("expected nil error to not be detected as user-not-found")
}
if !isUserDNNotFoundError(errors.New("user DN not found for: dillon")) {
t.Fatal("expected lowercase user-not-found error to be detected")
}
if !isUserDNNotFoundError(errors.New("User DN not found for: dillon")) {
t.Fatal("expected legacy uppercase user-not-found error to be detected")
}
if isUserDNNotFoundError(errors.New("base DN (dc=min,dc=io) for user DN search does not exist")) {
t.Fatal("expected infrastructure lookup error to not be detected as user-not-found")
}
}