-
Type:
Incident report
-
Resolution: Unresolved
-
Priority:
Trivial
-
None
-
Affects Version/s: 7.4.14, 8.0.0beta2
-
Component/s: Documentation (D), Packages (C), Server (S)
CLdap::checkCredentials() — the LDAP credential check used by both frontend login and the userdirectory.test API — performs two consecutive ldap_bind() calls with the same credentials. With direct user binding (%{user}{} placeholder in the Base DN), both binds carry the logging-in user's password. That is harmless for a static password, but it makes authentication impossible with any one-time bind credential: the first bind consumes the one-time password (OTP), and the second bind replays it and is rejected by the directory's OTP replay protection. The user cannot log in with valid, fresh credentials; Zabbix reports "Incorrect user name or password or account is temporarily blocked."
Non-MFA users work fine in direct-binding mode (their static password binds successfully twice), which makes the failure look credential-specific rather than structural.
Reproduced against authentik's LDAP outpost with in-band MFA (user binds require password;<6-digit TOTP>), on Zabbix 7.4.14 and 8.0.0beta2 (ui/include/classes/ldap/CLdap.php ). Applies to any directory that enforces single-use simple-bind credentials.
Steps to reproduce
- Run an LDAP directory whose user binds require a one-time component appended to the password. Example with authentik: deploy the authentik LDAP outpost (ldaps://<host>:636), enroll a user in TOTP; binds must use password;<totp> (in-band MFA).
- In Zabbix (Administration → Authentication → LDAP, or userdirectory.create + userdirectory.test via the API), configure direct user binding:
- Host ldaps://<host>, port 636
- Base DN containing the %{user} placeholder, e.g. cn=%{user},ou=users,<base DN> — this selects BIND_DNSTRING
- Bind DN / Bind password: empty
- Test the user with a fresh password;<totp> as the password.
Result: "Incorrect user name or password or account is temporarily blocked." On the directory side, two bind requests arrive back-to-back, both with the full password;<totp>: the first succeeds, the second is rejected as a replayed OTP because its TOTP time step was just consumed. The identical password;<totp> authenticates fine when a single bind is performed (ldapsearch, python-ldap3), and Zabbix's search-then-bind mode also succeeds — there the user's credential is used for exactly one bind.
Root cause
ui/include/classes/ldap/CLdap.php, checkCredentials() (7.4.14 = 8.0.0beta2):
^if (!$this->connect() || !$this->bind($user, $pass))
^
if (!$this->bind($user, $pass)) { // bind #2 — same code → rejected as replay
if ($this->bind_type == static::BIND_DNSTRING) {
$this->error = static::ERR_USER_NOT_FOUND;
}
return false;
}
The second bind() re-checks the just-successful bind with unchanged parameters. In the BIND_CONFIG_CREDENTIALS / anonymous modes it uses the configured service account (a static credential), so the redundancy goes unnoticed. In BIND_DNSTRING (direct user binding, selected by %{user} in the Base DN via initBindAttributes()), bind() substitutes the user's typed password for both calls ($dn_password = $password;), so any single-use credential fails deterministically on the second bind — both binds land in the same TOTP time step, so the code is byte-identical; no clock-skew or timing factor involved.
The double bind itself is longstanding; it only became harmful with 7.4's direct user binding, where the user's own credential is used for both binds.
Suggested fix
Drop the redundant second bind() call (the first call already established the bind), or otherwise bind exactly once per credential check.
Related: ZBX-27085 reports the same "bind again as the user" pattern in getUserAttributes() (a re-bind with a null password) breaking JIT group mapping with direct binding, closed as Won't fix / "by design". This report covers a distinct defect: the unconditional double bind in checkCredentials(), which breaks authentication itself, not provisioning.
Workaround
Use search-then-bind (a configured Bind DN/password service account) for directories with in-band MFA; direct user binding works only for users authenticated without MFA.