Quick Take
- What: CVE-2026-125321 in pgEdge Postgres MCP Server allowed attackers to bypass login rate limiting by spoofing the X-Forwarded-For header.
- Who is affected: Deployments of version 1.0.0 or earlier exposing the HTTP transport with authentication enabled. The HTTP transport is off by default.
- What to do: Upgrade to version 1.1.0 or later and review proxy configuration to ensure client IP resolution matches your deployment.
- Severity: CVSS 6.5 (Medium). Attackers could perform unlimited password guessing or temporarily block legitimate users.
- Fixed: Version 1.1.0 resolves client addresses from the connection and adds a trusted-proxies configuration block.
In plain English
A security flaw in pgEdge Postgres MCP Server let attackers trick the login system by sending fake IP addresses in a header called X-Forwarded-For. This meant they could guess passwords as many times as they wanted without being slowed down, or they could block real users from logging in by pretending to be them. The problem only affected servers that had the HTTP mode turned on with login protection enabled (HTTP mode is off by default). The company fixed this in version 1.1.0 by making the server use the real connection address instead of trusting what the attacker sends.
What you should do: If you run this server with HTTP mode on, update to 1.1.0 and check your proxy settings.
What happened
The pgEdge Postgres MCP Server, which enables SQL queries against PostgreSQL databases through Model Context Protocol compatible clients, contained a vulnerability in its login rate limiting mechanism. Prior to version 1.1.0, deployments exposing the non-default HTTP transport with authentication enabled used the leftmost caller-supplied X-Forwarded-For value as the client address for login rate limiting.
The function ExtractIPAddress in internal/auth/middleware.go read the leftmost entry of the X-Forwarded-For header first, which is precisely the part of that header a client can set for itself. This value was then supplied to IsAllowed and RecordFailedAttempt in internal/auth/rate_limiter.go, allowing an unauthenticated caller to rotate addresses to evade throttling entirely or assert another user’s address to consume that user’s limiter bucket.
The published nginx configuration examples in the project documentation used $proxy_add_x_forwarded_for, which appends to the caller’s value rather than replacing it. This made the vulnerability reachable without any operator misconfiguration. A client sending X-Forwarded-For: 9.9.9.9 would arrive as 9.9.9.9, real client, and the server would return 9.9.9.9.
The resolved address reached exactly two consumers: the login rate limiter and one request log line. The username-keyed account lockout was not affected because it is keyed on the username rather than the address.
What you should do
Upgrade to version 1.1.0 or later. The fix resolves the client address from the connection by default rather than from a caller-supplied header.
If you terminate TLS or proxy in front of the server, configure the new http.client_ip block to match your deployment. The configuration allows you to specify trusted proxies:
http:
client_ip:
source: header
header: X-Real-IP
trusted_proxies:
- 192.0.2.0/24The header is honored only when the peer matches trusted_proxies, and X-Forwarded-For is walked from right to left past trusted entries. Starting with source: header and no trusted proxies is a startup error rather than a silent fallback.
The updated nginx examples in the documentation now set X-Forwarded-For to $remote_addr rather than appending to it.
What it means
This vulnerability gave attackers two distinct abilities. First, they could evade the throttle entirely by varying the header on each attempt, meaning the limit was never reached and password guessing proceeded unthrottled. Second, they could throttle somebody else by asserting a victim’s address, failing the configured number of attempts, and causing the victim’s own requests to be refused for the length of the window.
The CVSS score of 6.5 reflects Medium severity with Low confidentiality and availability impact. No account lockout occurred because lockout is a separate, username-keyed mechanism that defaults to off (MaxFailedAttemptsBeforeLockout: 0).
The fix ensures that client addresses come from the connection by default, which is correct for a server clients reach directly and cannot be influenced by the caller. Resolution happens once per request in middleware, so the rate limiter and the request log agree on where a request came from.



