CVE-2026-7820 Overview
CVE-2026-7820 is an authentication control bypass in pgAdmin 4 versions prior to 9.15. The flaw stems from improper restriction of excessive authentication attempts [CWE-307]. pgAdmin enforces its MAX_LOGIN_ATTEMPTS setting only inside the custom /authenticate/login view. The Flask-Security default /login endpoint, automatically registered by security.init_app(), never consulted the User.locked field. An attacker who triggered an account lockout against an account using the INTERNAL authentication source could still obtain a valid session by submitting credentials directly to /login. The same path also lets attackers perform unbounded online password-guessing against INTERNAL accounts.
Critical Impact
Attackers bypass pgAdmin's brute-force lockout mechanism by targeting the Flask-Security /login endpoint, enabling unrestricted password guessing against any INTERNAL account.
Affected Products
- pgAdmin 4 versions prior to 9.15
- Deployments using the INTERNAL authentication source
- Installations relying on MAX_LOGIN_ATTEMPTS as the sole brute-force control
Discovery Timeline
- 2026-05-11 - CVE-2026-7820 published to NVD
- 2026-05-13 - Last updated in NVD database
Technical Details for CVE-2026-7820
Vulnerability Analysis
pgAdmin 4 exposes two authentication endpoints that share the same underlying user store. The custom /authenticate/login view filters by auth_source=INTERNAL and increments lockout counters when MAX_LOGIN_ATTEMPTS is exceeded. The Flask-Security default /login view is registered automatically during security.init_app() and remains reachable on every deployment. The two views do not share the same lockout enforcement logic, producing an inconsistent authentication policy across the application.
Root Cause
The User model relied on UserMixin.is_locked() from Flask-Security, which always returns not locked, and on Flask-Login's is_active, which only inspects the active column. Neither method consulted the locked column populated by /authenticate/login. As a result, the default /login view accepted valid credentials for accounts that pgAdmin's own logic considered locked. Login attempts arriving at /login were also never counted toward the MAX_LOGIN_ATTEMPTS threshold.
Attack Vector
An unauthenticated remote attacker submits repeated credential guesses directly to the /login endpoint. Because no rate limiting or lockout check applies, the attacker can iterate through password candidates without restriction. Even after /authenticate/login marks the targeted account as locked, valid credentials sent to /login still produce an authenticated session. LDAP, OAuth2, Kerberos, and Webserver users are not affected because Flask-Security's LoginForm.validate rejects them before any lockout check, and the lockout itself is filtered to auth_source=INTERNAL.
No verified public exploit code is available. See the pgAdmin GitHub Issue Tracker Entry for technical details.
Detection Methods for CVE-2026-7820
Indicators of Compromise
- Repeated HTTP POST requests to /login from a single source address with varying credential payloads.
- Successful authentications to /login for accounts that have an active lockout flag recorded in the pgAdmin user table.
- Web server logs showing high request volume to /login without corresponding entries in /authenticate/login.
Detection Strategies
- Correlate authentication telemetry from both /login and /authenticate/login to surface accounts authenticated through the unprotected path.
- Query the pgAdmin user database for sessions established on accounts where locked=true.
- Alert on authentication attempt rates that exceed the configured MAX_LOGIN_ATTEMPTS value within a short time window per source IP.
Monitoring Recommendations
- Forward pgAdmin web server access logs to a centralized log analytics platform for sustained inspection.
- Track per-account failed login counts across both endpoints rather than relying on application-side counters alone.
- Monitor for session creation events that occur shortly after lockout events for the same user identifier.
How to Mitigate CVE-2026-7820
Immediate Actions Required
- Upgrade pgAdmin 4 to version 9.15 or later, which overrides User.is_active and User.is_locked() to enforce the locked column on every authentication path.
- Audit INTERNAL accounts for recent successful authentications during periods when the account was locked.
- Rotate passwords for any INTERNAL account that shows signs of unbounded login activity against /login.
Patch Information
The upstream fix overrides User.is_active and User.is_locked() so the locked column is consulted by both /authenticate/login and the Flask-Security default /login view. The patch is included in pgAdmin 4 release 9.15. Refer to the pgAdmin GitHub Issue Tracker Entry for the complete change set and review history.
Workarounds
- Restrict network access to pgAdmin to trusted administrative networks until the upgrade is applied.
- Place a reverse proxy or web application firewall in front of pgAdmin to rate-limit requests to /login.
- Migrate users away from the INTERNAL authentication source to LDAP, OAuth2, Kerberos, or Webserver authentication, which are not affected by this bypass.
# Example nginx rate-limit configuration in front of pgAdmin
limit_req_zone $binary_remote_addr zone=pglogin:10m rate=5r/m;
location /login {
limit_req zone=pglogin burst=5 nodelay;
proxy_pass http://pgadmin_backend;
}
location /authenticate/login {
limit_req zone=pglogin burst=5 nodelay;
proxy_pass http://pgadmin_backend;
}
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.


