CVE-2026-33419: LDAP STS Enumeration and the Throttling Chain
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 InvalidParameterValueSTS 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:
- 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.
- Untrusted proxy headers: The limiter must default to the TCP socket peer (
RemoteAddr) and ignore client-suppliedX-Forwarded-For,X-Real-IP, andForwardedheaders unless the socket peer matches an explicit trusted-proxy allowlist. - 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
LDAPUsernamefor other origins; - peeled trusted hops from
X-Forwarded-Forright-to-left and selected the first untrusted client address; - rejected catch-all trusted-proxy CIDRs (
0.0.0.0/0and::/0) that would allow any client to spoofX-Forwarded-For; - dropped RFC 7239
Forwardedfrom security-sensitive rate-limit bucketing; - accepted
X-Real-IPonly 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 InvalidParameterValueresponse is what closes the username enumeration oracle.