CVE-2026-11922 Overview
CVE-2026-11922 affects zenml-io/zenml versions 0.57.0 through 0.94.2. The vulnerability allows attackers to bypass rate-limiting on the POST /api/v1/login endpoint and self password-change endpoints by rotating the X-Forwarded-For header. The rate limiter keys requests by request.client.host, which is populated from the X-Forwarded-For header when Uvicorn runs with --proxy-headers --forwarded-allow-ips *. This design lets clients control the rate-limiter key, enabling unthrottled credential guessing attacks. The issue is categorized under [CWE-290] Authentication Bypass by Spoofing.
Critical Impact
Attackers can execute unrestricted brute-force attempts against authentication endpoints by rotating a client-controlled header, exposing accounts to credential guessing.
Affected Products
- zenml-io/zenml version 0.57.0 (lower bound)
- zenml-io/zenml versions 0.57.0 through 0.94.2
- Deployments running Uvicorn with --proxy-headers --forwarded-allow-ips *
Discovery Timeline
- 2026-07-24 - CVE-2026-11922 published to NVD
- 2026-07-24 - Last updated in NVD database
Technical Details for CVE-2026-11922
Vulnerability Analysis
The ZenML server uses a rate limiter that identifies clients by IP address to throttle authentication attempts. The _get_ipaddr method in src/zenml/zen_server/rate_limit.py reads the X-Forwarded-For header before falling back to request.client.host. When Uvicorn is launched with --proxy-headers --forwarded-allow-ips *, the ASGI layer already trusts and rewrites request.client.host from X-Forwarded-For. Both code paths end up honoring an attacker-controlled header.
An attacker can rotate the X-Forwarded-For value on every request, producing a new rate-limit key each time. The limiter never accumulates failed attempts against a single identifier, so throttling never triggers on the POST /api/v1/login endpoint or the self password-change endpoints.
Root Cause
The root cause is trust in a client-controlled header for security-critical keying. The permissive --forwarded-allow-ips * configuration accepts X-Forwarded-For from any source, and the rate limiter compounds the problem by re-reading the header directly. Neither layer validates that the request originated from a trusted reverse proxy.
Attack Vector
The attack is remote, requires no authentication, and no user interaction. An attacker sends repeated login attempts to POST /api/v1/login, changing the X-Forwarded-For header on each request to defeat the per-IP counter.
def _get_ipaddr(self, request: Request) -> str:
"""Returns the IP address for the current request.
- Based on the X-Forwarded-For headers or client information.
+ Based on the client information provided by the ASGI server.
Args:
request: The request object.
Returns:
The ip address for the current request (or 127.0.0.1 if none found).
"""
- if "X_FORWARDED_FOR" in request.headers:
- return request.headers["X_FORWARDED_FOR"]
- else:
- if not request.client or not request.client.host:
- return "127.0.0.1"
+ if not request.client or not request.client.host:
+ return "127.0.0.1"
- return request.client.host
+ return request.client.host
Source: GitHub Commit 8a2214b
The patch removes direct reading of X_FORWARDED_FOR from request headers and relies on client information supplied by the ASGI server, with proxy trust configured explicitly at deployment time.
Detection Methods for CVE-2026-11922
Indicators of Compromise
- High volume of POST /api/v1/login requests from the same TCP source with varying X-Forwarded-For values.
- Successive failed authentication events for one or many usernames without corresponding rate-limit rejections.
- Requests to self password-change endpoints containing spoofed or private-range X-Forwarded-For addresses.
Detection Strategies
- Correlate authentication failures by source TCP address rather than by the X-Forwarded-For header value.
- Flag any client that presents more than a threshold number of distinct X-Forwarded-For values within a short window.
- Alert when ZenML login failures spike without matching HTTP 429 rate-limit responses.
Monitoring Recommendations
- Ingest ZenML server access logs and Uvicorn proxy logs into a centralized analytics pipeline.
- Monitor the ratio of HTTP 401 to HTTP 429 responses on /api/v1/login; a shrinking 429 count indicates bypass.
- Track the diversity of X-Forwarded-For header values per socket peer to detect header rotation.
How to Mitigate CVE-2026-11922
Immediate Actions Required
- Upgrade zenml-io/zenml to a release later than 0.94.2 that includes commit 8a2214b.
- Remove --forwarded-allow-ips * from Uvicorn startup and restrict trusted proxy IPs to known reverse-proxy addresses.
- Enforce rate limiting at the reverse proxy or ingress layer as a defense-in-depth control.
- Review authentication logs for evidence of brute-force activity predating the patch.
Patch Information
The fix is delivered in GitHub Commit 8a2214b. It removes the direct read of X_FORWARDED_FOR in rate_limit.py and updates Helm templates to align with the corrected trust model. Additional context is available on the Huntr Bounty Listing.
Workarounds
- Configure Uvicorn with an explicit allow-list of reverse-proxy IPs instead of *.
- Apply IP-based throttling at NGINX, HAProxy, or an API gateway using the real source IP.
- Require multi-factor authentication on ZenML accounts to reduce the value of credential guessing.
# Configuration example - restrict trusted proxies at the ASGI layer
uvicorn zenml.zen_server.zen_server_api:app \
--host 0.0.0.0 \
--port 8080 \
--proxy-headers \
--forwarded-allow-ips "10.0.0.5,10.0.0.6"
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

