# CVE-2026-86863 — pgAdmin 4 Webserver Authentication Bypass ## Exploit Intelligence Platform | | | | --- | --- | | **CVE** | CVE-2026-86863 | | **Component** | pgAdmin 4 — Webserver authentication source (`web/pgadmin/authenticate/webserver.py`) | | **Vulnerability type** | CWE-290 Authentication Bypass by Spoofing; CWE-807 Reliance on Untrusted Inputs in a Security Decision | | **CVSS v4.0** | 9.3 CRITICAL — `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N` | | **EPSS** | 0.00583 | | **Affected versions** | 6.2 .. 9.17 | | **Fixed in** | 9.18 (fix commit `02e327c1b9988bb88ca87729f6dc646f99822f7e`) | | **Author** | Exploit Intelligence Platform | | **Date** | 2026-09-29 | ## Vulnerability summary pgAdmin 4's *Webserver* authentication source is designed to accept an identity that a trusted reverse proxy in front of pgAdmin asserts. In the vulnerable versions, `WebserverAuthentication.get_user()` read that identity from the WSGI environment and, when the lookup returned nothing, fell back to reading the same name straight from the inbound HTTP request headers: ```python def get_user(self): username = request.environ.get(config.WEBSERVER_REMOTE_USER) if not username: # One more try to get the Remote User from the hearders username = request.headers.get(config.WEBSERVER_REMOTE_USER) return username ``` An inbound header is written by whoever sends the request, so the fallback trusts the client. The environment lookup is no better: under WSGI an inbound header `X-Forwarded-User: ` is copied into the environment as `HTTP_X_FORWARDED_USER`, so when `WEBSERVER_REMOTE_USER` is set to that documented value, the *first* lookup is already the attacker's own header. `login()` then compounded the problem: it looked the asserted username up and logged it in with no check on the account's authentication source, so asserting the name of an existing **internal-auth Administrator** logged the attacker in as that administrator. ## Attack chain 1. Deployment precondition: `AUTHENTICATION_SOURCES` includes `webserver` and `WEBSERVER_REMOTE_USER` is a header-satisfiable name (`HTTP_X_FORWARDED_USER` or `X-Forwarded-User`). Default pgAdmin uses `['internal']` and is not affected. 2. The attacker sends `POST /authenticate/login` with an **empty body** and one header: `X-Forwarded-User: `. No password, no CSRF token, and no prior session are required — the login route is CSRF-exempt by design. 3. pgAdmin treats the header as a proxy-asserted identity and logs the session in as the named account, redirecting to `/browser/`. 4. `GET /browser/` with the returned session answers `200` with the authenticated application shell, and the impersonated name appears in the `X-Remote-User` response header and in pgAdmin's own access log. ## Fix analysis Release 9.18 separates the trusted source from the untrusted one: - `_is_cgi_var_name()` accepts only a bare CGI variable name; anything `HTTP_`-prefixed or hyphenated is treated as header-derived and never read from the environment. - The header fallback requires **all** of: `WEBSERVER_REMOTE_USER_FROM_HEADER` (default `False`), a peer listed in `WEBSERVER_TRUSTED_PROXIES` (default empty), and a matching shared secret when `WEBSERVER_SHARED_SECRET` is set. The secret is compared with `hmac.compare_digest`. - The trusted-peer check reads the raw pre-`ProxyFix` socket peer from `werkzeug.proxy_fix.orig`, not `request.remote_addr`, because `ProxyFix` rewrites the latter from the client-controlled `X-Forwarded-For`. - `login()` refuses `user is None` and refuses any account whose `auth_source` is not `webserver`. No bypass of this fix was found (see `bypass_analysis.md`). ## Lab run instructions `seed.sh` verifies the seeded state rather than mutating it: pgAdmin initialises its own database from the container environment on first start, so there is nothing to install. ```bash # Vulnerable environment (pgAdmin 4 9.17) docker compose --env-file .env -p cve-2026-86863-lab -f docker-compose.yml up -d --build bash seed.sh lab # Fixed control environment (fix commit 02e327c1b, as released in 9.18) docker compose --env-file .env -p cve-2026-86863-control -f docker-compose.control.yml up -d --build bash seed.sh control ``` Run the PoC. **Implementation:** the packaged PoC is a Python 3 stdlib-only entrypoint. **Runtime:** any Python 3.9+ interpreter; the examples below use the runtime container's own interpreter so that no client install is needed and the `container-controller` loopback constraint is respected by attaching to the Compose network. ```bash # Vulnerable target — expected final line: [SUCCESS] docker run --rm -i --network cve-2026-86863-lab_default --entrypoint python3 \ dpage/pgadmin4:9.18 -u - http://cve-2026-86863-lab-web-1:5050 < poc/poc.py # Fixed control — expected final line: [FAILED] (the header is ignored) docker run --rm -i --network cve-2026-86863-control_default --entrypoint python3 \ dpage/pgadmin4:9.18 -u - http://cve-2026-86863-control-web-1:5050 < poc/poc.py ``` `poc/poc.py` accepts the base URL positionally and an optional `--impersonate `. Tear down: ```bash docker compose --env-file .env -p cve-2026-86863-lab -f docker-compose.yml down -v docker compose --env-file .env -p cve-2026-86863-control -f docker-compose.control.yml down -v ``` ## Reproducibility 3/3 successful from cold state (each cycle: `down -v`, fresh `up`, health gate, seed verification, PoC run). The fixed control is a genuine negative produced by the real fixed build with identical configuration. ## Notes for operators upgrading to 9.18 Both are fail-closed and neither is a bypass, but both will surprise a migrating operator: - The value `HTTP_X_FORWARDED_USER` — the advisory's own example — can never satisfy the fixed header path, because Werkzeug's `Headers.get('HTTP_X_FORWARDED_USER')` does not match an inbound `X-Forwarded-User` header. Use the hyphenated `X-Forwarded-User` for the opt-in header mode. - `WEBSERVER_TRUSTED_PROXIES` entries must match the address form the kernel reports. A peer seen as `::ffff:172.20.0.3` (IPv4-mapped IPv6) will not match an `172.16.0.0/12` entry; list the mapped form instead. ## References - https://nvd.nist.gov/vuln/detail/CVE-2026-86863 (GHSA-RC4W-FWH9-F5J9) - https://github.com/pgadmin-org/pgadmin4/issues/10383 - https://github.com/pgadmin-org/pgadmin4/commit/02e327c1b9988bb88ca87729f6dc646f99822f7e