Skip to content

CVE-2026-33419: LDAP STS Enumeration and the Throttling Chain

From uniform authentication failure responses to reservation refunds, proxy source attribution, and removing the shared username bucket: how LDAP STS hardening evolved across three iterations.

Status: Released, followed by two rounds of hardening
First containing release: RELEASE.2026-04-17T00-00-00Z
Complete correction: RELEASE.2026-06-18T00-00-00Z
Fix commits: 3b950f8, 18b712d, 9e10f6d, f441108, 5e40665
GitHub issue: pgsty/silo#23

The original defect in AssumeRoleWithLDAPIdentity was straightforward: LDAP STS returned distinct errors for “user does not exist” and “password is wrong,” creating an unauthenticated username oracle. Our first patch unified the external authentication failure response and added rate-limit buckets keyed by both source IP and username.

Subsequent design reviews uncovered a deeper problem: naive success refunds, spoofable proxy headers, reservation accounting, and the shared username bucket itself turned the rate limiter into a targeted denial-of-service primitive. The final June design (5e40665) removed the shared username bucket that enabled cross-origin account lockouts, retained the per-source-IP bucket, and formalized proxy IP attribution as an explicit deployment contract.

Initial threat model

The vulnerable entry point is AssumeRoleWithLDAPIdentity. An attacker requires no existing S3 or LDAP credentials; network access to the STS endpoint is sufficient to compare the HTTP status code and error message returned for an unknown username versus a wrong password, enumerate valid directory accounts, and follow up with password spraying.

At the same time, remediation could not flatten every LDAP error into “invalid credentials.” Directory connection failures, lookup-bind credential errors, and LDAP server timeouts must still surface as HTTP 500 infrastructure errors so operators can detect and diagnose directory outages.

First round: uniform responses and a dual-key limiter

The initial patch on 2026-04-15 (3b950f8) introduced three changes:

  • unknown-user lookups and bad-password binds both returned the same external 400 InvalidParameterValue STS authentication error;
  • LDAP infrastructure failures continued to return HTTP 500, logging the underlying directory error on the server;
  • a new in-memory limiter throttled authentication attempts using buckets for both client source IP and normalized LDAPUsername.

While this closed the response-content oracle and raised the cost of brute-force guessing, reviewing the limiter’s state machine and IP extraction immediately exposed flaws in how tokens were consumed and attributed.

Second round: success refunds, proxy attribution, and token accounting

Follow-up commits on April 16 (18b712d, 9e10f6d, f441108) fixed three defects in the initial limiter:

  1. Explicit reserve/commit/cancel/refund lifecycle: Successful authentication and LDAP infrastructure errors must refund their reserved token so legitimate high-frequency logins do not exhaust the failure allowance.
  2. Untrusted proxy headers: The limiter must default to the TCP socket peer (RemoteAddr) and ignore client-supplied X-Forwarded-For, X-Real-IP, and Forwarded headers unless the socket peer matches an explicit trusted-proxy allowlist.
  3. Bounded token refunds: Cancellation and refund paths must clamp token counts to bucket capacity so concurrent cancellations cannot mint extra capacity.

Third round: why we deleted the username bucket

A second design review in June overturned our initial assumption that “throttling by source IP plus username is strictly stronger than throttling by source IP alone.”

Because the per-username bucket was shared globally across all client origins on a node, an attacker who knew a victim’s LDAPUsername could exhaust that user’s bucket from any IP address (or across rotating IPs). With only a handful of failed requests per minute, an attacker could keep a targeted user’s AssumeRoleWithLDAPIdentity calls permanently throttled before the legitimate user’s request ever reached an LDAP bind.

Commit 5e40665 (shipped in RELEASE.2026-06-18T00-00-00Z) therefore made five adjustments:

  • deleted the shared per-username bucket so one origin cannot lock out a specific LDAPUsername for other origins;
  • peeled trusted hops from X-Forwarded-For right-to-left and selected the first untrusted client address;
  • rejected catch-all trusted-proxy CIDRs (0.0.0.0/0 and ::/0) that would allow any client to spoof X-Forwarded-For;
  • dropped RFC 7239 Forwarded from security-sensitive rate-limit bucketing;
  • accepted X-Real-IP only when the immediate peer is a configured trusted proxy that overwrites the header.

This proxy trust model was subsequently generalized across all S3 and Admin handlers as SN-2026-018 (MINIO_API_TRUSTED_PROXIES).

Rejected alternatives

Option Why it was rejected
Perform a dummy LDAP bind for unknown users Amplifies load on the LDAP directory and adds a second bind path after the response-content oracle is already closed
Bucket all IPv6 clients by /64 prefix Causes unrelated users behind the same enterprise or carrier /64 prefix to throttle one another
Take the leftmost X-Forwarded-For value The leftmost entry is controlled by the client and trivially spoofed
Fall back to the socket peer when X-Forwarded-For and X-Real-IP disagree An attacker can intentionally inject a conflicting header to force fallback and collapse all users behind a proxy into a single bucket
Parse RFC 7239 Forwarded for rate limiting High parser complexity and ambiguity with no practical security advantage over X-Forwarded-For and X-Real-IP

Verification and release

The unit test suite verifies the limiter’s reserve/commit/cancel/refund state machine, concurrent access, token refunds on successful binds and LDAP infrastructure errors, external response equivalence between unknown users and bad passwords, and source-IP extraction across direct RemoteAddr, spoofed headers, single- and multi-hop trusted proxies, and rejected catch-all CIDRs (0.0.0.0/0, ::/0).

Note that the end-to-end LDAP security test skips live directory binds when _MINIO_LDAP_TEST_SERVER is unset; full end-to-end verification requires pointing _MINIO_LDAP_TEST_SERVER at a live LDAP instance in addition to running the package unit tests.

Final trade-offs and operational boundaries

  • Source-IP-only throttling: Removing the username bucket prevents targeted cross-IP account lockouts, at the cost of no longer enforcing a single-account failure cap across distributed botnet origins.
  • Per-node memory state: The limiter is per-node and in-memory rather than a cluster-wide password defense; distributed spraying and LDAP bind timing differences still require directory-side policies or edge rate limiting.
  • Proxy configuration: Deployments behind a reverse proxy must configure trusted proxy CIDRs (MINIO_API_TRUSTED_PROXIES) so legitimate clients behind the proxy are bucketed by their client IP rather than the proxy’s socket address.
  • Uniform responses are the primary defense: Rate limiting slows down guessing from a single source, while the unified 400 InvalidParameterValue response is what closes the username enumeration oracle.