LDAP bind credentials transmitted in plaintext (no TLS) #95

Closed
opened 2026-09-01 00:13:37 -04:00 by jared · 1 comment
Owner

Severity: Medium

api/user_avatar.php (~line 116) connects via ldap://$ldapHost:$ldapPort — never ldaps://, and there's no ldap_start_tls() call anywhere in the file or codebase. LDAP_BIND_PW (from .env) is sent over the wire unencrypted on every avatar fetch.

Impact: If LDAP_HOST isn't reached over an already-fully-trusted, isolated network segment, the bind password is sniffable by anything with visibility into that network path. Not currently logged anywhere (verified: the only error_log() on bind failure logs ldap_error()'s generic message, not the password), so this is purely a transport-security gap, not a credential-logging one.

Fix: Switch to ldaps:// (if the LDAP server supports it) or add an ldap_start_tls() call before binding, with LDAP_PORT/config updated accordingly.

**Severity:** Medium `api/user_avatar.php` (~line 116) connects via `ldap://$ldapHost:$ldapPort` — never `ldaps://`, and there's no `ldap_start_tls()` call anywhere in the file or codebase. `LDAP_BIND_PW` (from `.env`) is sent over the wire unencrypted on every avatar fetch. **Impact:** If `LDAP_HOST` isn't reached over an already-fully-trusted, isolated network segment, the bind password is sniffable by anything with visibility into that network path. Not currently logged anywhere (verified: the only `error_log()` on bind failure logs `ldap_error()`'s generic message, not the password), so this is purely a transport-security gap, not a credential-logging one. **Fix:** Switch to `ldaps://` (if the LDAP server supports it) or add an `ldap_start_tls()` call before binding, with LDAP_PORT/config updated accordingly.
jared added the priority/mediumsecurity labels 2026-09-08 10:15:50 -04:00
Author
Owner

Fixed in commit 863f84f (merged to main in 74544ac), plus infra changes deployed alongside it.

api/user_avatar.php connected via ldap://$ldapHost:$ldapPort -- never ldaps://, so LDAP_BIND_PW went over the wire unencrypted on every avatar fetch.

Switched to ldaps://, and changed LDAP_HOST/LDAP_PORT to ldap.lotusguild.org:6360 (lldap'''s LDAPS listener) instead of the bare IP on port 3890. PHP'''s ldap extension verifies the server cert'''s hostname by default, so a bare IP won'''t validate against the LDAPS cert (issued for *.lotusguild.org) -- LDAP_HOST has to be a hostname the cert actually covers.

Two infra changes were needed to make this work, not just the code:

  1. Pi-hole split-horizon DNS override (pve-infra repo) so ldap.lotusguild.org resolves internally to the real LDAP server'''s LAN IP -- its existing public DNS record points at an unrelated host, and there was no internal-only entry for it before.
  2. Production'''s .env explicitly set LDAP_HOST=10.10.10.39 and LDAP_PORT=3890 (the old plaintext values), which would have silently overridden the new secure code defaults. Updated to LDAP_HOST=ldap.lotusguild.org / LDAP_PORT=6360 (old file backed up alongside it).

Verified end-to-end against the real lldap server (pct 147) and the real production tinker_tickets host (10.10.10.45): confirmed the Pi-hole override resolves correctly, then ran the actual ldap_connect/ldap_bind sequence on production with the real LDAP_BIND_DN/LDAP_BIND_PW from its live config -- got a successful bind over a verified LDAPS connection (valid Let'''s Encrypt cert, chain + hostname both checked, no verification bypass).

Also noticed while deploying: production'''s webhook-based auto-deploy didn'''t pick up the push to main on its own (had to run tinker_deploy.sh manually to get the fix live) -- separate issue from this one, flagging in case it'''s worth looking at.

Fixed in commit 863f84f (merged to main in 74544ac), plus infra changes deployed alongside it. api/user_avatar.php connected via ldap://$ldapHost:$ldapPort -- never ldaps://, so LDAP_BIND_PW went over the wire unencrypted on every avatar fetch. Switched to ldaps://, and changed LDAP_HOST/LDAP_PORT to ldap.lotusguild.org:6360 (lldap'''s LDAPS listener) instead of the bare IP on port 3890. PHP'''s ldap extension verifies the server cert'''s hostname by default, so a bare IP won'''t validate against the LDAPS cert (issued for *.lotusguild.org) -- LDAP_HOST has to be a hostname the cert actually covers. Two infra changes were needed to make this work, not just the code: 1. Pi-hole split-horizon DNS override (pve-infra repo) so ldap.lotusguild.org resolves internally to the real LDAP server'''s LAN IP -- its existing public DNS record points at an unrelated host, and there was no internal-only entry for it before. 2. Production'''s .env explicitly set LDAP_HOST=10.10.10.39 and LDAP_PORT=3890 (the old plaintext values), which would have silently overridden the new secure code defaults. Updated to LDAP_HOST=ldap.lotusguild.org / LDAP_PORT=6360 (old file backed up alongside it). Verified end-to-end against the real lldap server (pct 147) and the real production tinker_tickets host (10.10.10.45): confirmed the Pi-hole override resolves correctly, then ran the actual ldap_connect/ldap_bind sequence on production with the real LDAP_BIND_DN/LDAP_BIND_PW from its live config -- got a successful bind over a verified LDAPS connection (valid Let'''s Encrypt cert, chain + hostname both checked, no verification bypass). Also noticed while deploying: production'''s webhook-based auto-deploy didn'''t pick up the push to main on its own (had to run tinker_deploy.sh manually to get the fix live) -- separate issue from this one, flagging in case it'''s worth looking at.
jared closed this issue 2026-09-12 01:02:18 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/tinker_tickets#95