From 483aa80e4064f2a858c1b6a6737cd4f5b5fc1276 Mon Sep 17 00:00:00 2001 From: Matt Hook Date: Wed, 17 Apr 2024 16:08:48 +1200 Subject: [PATCH] fix(auth): prevent user enumeration attack [EE-6832] (#11588) --- api/http/handler/auth/authenticate.go | 13 +++++++++++-- api/ldap/ldap.go | 9 ++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/api/http/handler/auth/authenticate.go b/api/http/handler/auth/authenticate.go index b3908e87b..4547d7795 100644 --- a/api/http/handler/auth/authenticate.go +++ b/api/http/handler/auth/authenticate.go @@ -75,7 +75,12 @@ func (handler *Handler) authenticate(rw http.ResponseWriter, r *http.Request) *h if settings.AuthenticationMethod == portainer.AuthenticationInternal || settings.AuthenticationMethod == portainer.AuthenticationOAuth || (settings.AuthenticationMethod == portainer.AuthenticationLDAP && !settings.LDAPSettings.AutoCreateUsers) { - return httperror.NewError(http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized) + // avoid username enumeration timing attack by creating a fake user + // https://en.wikipedia.org/wiki/Timing_attack + user = &portainer.User{ + Username: "portainer-fake-username", + Password: "$2a$10$abcdefghijklmnopqrstuvwx..ABCDEFGHIJKLMNOPQRSTUVWXYZ12", // fake but valid format bcrypt hash + } } } @@ -112,7 +117,11 @@ func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portai func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError { err := handler.LDAPService.AuthenticateUser(username, password, ldapSettings) if err != nil { - return httperror.Forbidden("Only initial admin is allowed to login without oauth", err) + if errors.Is(err, httperrors.ErrUnauthorized) { + return httperror.NewError(http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized) + } + + return httperror.InternalServerError("Unable to authenticate user against LDAP", err) } if user == nil { diff --git a/api/ldap/ldap.go b/api/ldap/ldap.go index 21358816c..6202c716a 100644 --- a/api/ldap/ldap.go +++ b/api/ldap/ldap.go @@ -75,7 +75,14 @@ func (*Service) AuthenticateUser(username, password string, settings *portainer. userDN, err := searchUser(username, connection, settings.SearchSettings) if err != nil { - return err + if errors.Is(err, errUserNotFound) { + // prevent user enumeration timing attack by attempting the bind with a fake user + // and whatever password was provided should definately fail + // https://en.wikipedia.org/wiki/Timing_attack + userDN = "portainer-fake-ldap-username" + } else { + return err + } } err = connection.Bind(userDN, password)