FKIE_CVE-2026-12044
Vulnerability from fkie_nvd - Published: 2026-06-19 00:16 - Updated: 2026-10-26 11:10
Severity
Summary
SQL injection in pgAdmin 4 across every dialog template that renders ``COMMENT ON ... IS '<description>'`` for a user-supplied description field. The Jinja templates for Domains (and their constraints), Foreign Tables, Languages, and Event Triggers, plus the Views OID-lookup query, interpolated the description directly inside a single-quoted SQL literal -- ``'{{ data.description }}'`` -- instead of passing it through the ``qtLiteral`` escape filter. An authenticated pgAdmin user with permission to create or alter the affected object types could submit a description containing an apostrophe, break out of the literal and chain arbitrary SQL. The injected SQL runs under the PostgreSQL role the user is already authenticated as; for a connected role with ``COPY ... TO/FROM PROGRAM`` (typically PostgreSQL superuser), this chains to OS command execution on the PostgreSQL host. The defect does not cross a privilege boundary -- the user already has direct SQL access to that role through pgAdmin's Query Tool -- so the attacker gains no capability beyond what their database role already grants. The marginal impact captures bypass of any application-layer Query Tool gating an operator may have configured.
The defect was originally reported against the Domain Dialog ``description`` field; a code-wide audit identified sixteen sites of the same pattern across the templates listed above. The same review also surfaced ten related sinks in the pgstattuple/pgstatindex stats templates -- ``pgstattuple('{{schema}}.{{table}}')`` and the matching pgstatindex shape -- where ``qtIdent`` escapes embedded double quotes inside the identifier but not apostrophes, so a user with CREATE privilege on a schema could plant a table or index named ``foo'bar`` and a later stats viewer would render an unbalanced literal.
Fix is layered:
1. Sites: replace every ``'{{ x.description }}'`` with ``{{ x.description|qtLiteral(conn) }}`` (no surrounding quotes -- the filter wraps the value in escaped quotes itself). Plumb ``conn=self.conn`` through every ``render_template`` call that loads one of these templates. Also corrects a ``{ % elif`` Jinja typo in the foreign-table schema diff (dead branch). Rewrite the ten pgstattuple/pgstatindex stats sites to address the relation via OID + ``::oid::regclass`` cast (e.g. ``pgstattuple({{ tid }}::oid::regclass)``), eliminating the embedded literal-call form entirely so that bug-class can no longer recur there.
2. Driver hardening: ``qtLiteral`` (in ``utils/driver/psycopg3/__init__.py``) used to silently return the raw unescaped value when its ``conn`` argument was falsy. It now raises ``ValueError`` -- surfacing the entire bug class going forward. The change immediately uncovered eight latent plumbing bugs (in ``schemas/__init__.py``, ``schemas/functions/__init__.py``, ``schemas/tables/utils.py``, ``foreign_servers/__init__.py``, and seven sites in ``roles/__init__.py``) -- all fixed as part of this patch. The inner ``except`` block that swallowed adapter-level failures and returned the raw value is also removed, so unadaptable inputs raise instead of leaking unescaped values.
3. Regression tests: a per-template behavioural test renders each previously-vulnerable template with an apostrophe-injection payload and asserts the escaped fragment is present and the vulnerable fragment absent; a lint test walks every ``*.sql`` template flagging any ``'{{ ... }}'`` single-quote-wrapped interpolation against an explicit allowlist; unit tests cover the new qtLiteral fail-fast and inner-except raise paths.
This issue affects pgAdmin 4: from 1.0 before 9.16.
References
{
"affected": [
{
"affectedData": [
{
"defaultStatus": "unaffected",
"modules": [
"Domains",
"Domain Constraints",
"Foreign Tables",
"Languages",
"Event Triggers",
"Views",
"Tables",
"Indexes",
"Index Constraints",
"Exclusion Constraints",
"Materialized Views",
"Driver (qtLiteral)"
],
"product": "pgAdmin 4",
"programFiles": [
"https://github.com/pgadmin-org/pgadmin4/blob/master/web/pgadmin/utils/driver/psycopg3/__init__.py",
"https://github.com/pgadmin-org/pgadmin4/blob/master/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/templates/domains/sql/default/create.sql",
"https://github.com/pgadmin-org/pgadmin4/blob/master/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/16_plus/stats.sql"
],
"repo": "https://github.com/pgadmin-org/pgadmin4",
"vendor": "pgadmin.org",
"versions": [
{
"lessThan": "9.16",
"status": "affected",
"version": "1.0",
"versionType": "custom"
}
]
}
],
"source": "f86ef6dc-4d3a-42ad-8f28-e6d5547a5007"
}
],
"configurations": [
{
"nodes": [
{
"cpeMatch": [
{
"criteria": "cpe:2.3:a:pgadmin:pgadmin_4:*:*:*:*:*:postgresql:*:*",
"matchCriteriaId": "6C00C802-CCFB-4A7B-835E-FBE61FFC87FD",
"versionEndExcluding": "9.16",
"versionStartIncluding": "1.0",
"vulnerable": true
}
],
"negate": false,
"operator": "OR"
}
]
}
],
"cveTags": [],
"descriptions": [
{
"lang": "en",
"value": "SQL injection in pgAdmin 4 across every dialog template that renders ``COMMENT ON ... IS \u0027\u003cdescription\u003e\u0027`` for a user-supplied description field. The Jinja templates for Domains (and their constraints), Foreign Tables, Languages, and Event Triggers, plus the Views OID-lookup query, interpolated the description directly inside a single-quoted SQL literal -- ``\u0027{{ data.description }}\u0027`` -- instead of passing it through the ``qtLiteral`` escape filter. An authenticated pgAdmin user with permission to create or alter the affected object types could submit a description containing an apostrophe, break out of the literal and chain arbitrary SQL. The injected SQL runs under the PostgreSQL role the user is already authenticated as; for a connected role with ``COPY ... TO/FROM PROGRAM`` (typically PostgreSQL superuser), this chains to OS command execution on the PostgreSQL host. The defect does not cross a privilege boundary -- the user already has direct SQL access to that role through pgAdmin\u0027s Query Tool -- so the attacker gains no capability beyond what their database role already grants. The marginal impact captures bypass of any application-layer Query Tool gating an operator may have configured.\n\nThe defect was originally reported against the Domain Dialog ``description`` field; a code-wide audit identified sixteen sites of the same pattern across the templates listed above. The same review also surfaced ten related sinks in the pgstattuple/pgstatindex stats templates -- ``pgstattuple(\u0027{{schema}}.{{table}}\u0027)`` and the matching pgstatindex shape -- where ``qtIdent`` escapes embedded double quotes inside the identifier but not apostrophes, so a user with CREATE privilege on a schema could plant a table or index named ``foo\u0027bar`` and a later stats viewer would render an unbalanced literal.\n\nFix is layered:\n\n 1. Sites: replace every ``\u0027{{ x.description }}\u0027`` with ``{{ x.description|qtLiteral(conn) }}`` (no surrounding quotes -- the filter wraps the value in escaped quotes itself). Plumb ``conn=self.conn`` through every ``render_template`` call that loads one of these templates. Also corrects a ``{ % elif`` Jinja typo in the foreign-table schema diff (dead branch). Rewrite the ten pgstattuple/pgstatindex stats sites to address the relation via OID + ``::oid::regclass`` cast (e.g. ``pgstattuple({{ tid }}::oid::regclass)``), eliminating the embedded literal-call form entirely so that bug-class can no longer recur there.\n\n 2. Driver hardening: ``qtLiteral`` (in ``utils/driver/psycopg3/__init__.py``) used to silently return the raw unescaped value when its ``conn`` argument was falsy. It now raises ``ValueError`` -- surfacing the entire bug class going forward. The change immediately uncovered eight latent plumbing bugs (in ``schemas/__init__.py``, ``schemas/functions/__init__.py``, ``schemas/tables/utils.py``, ``foreign_servers/__init__.py``, and seven sites in ``roles/__init__.py``) -- all fixed as part of this patch. The inner ``except`` block that swallowed adapter-level failures and returned the raw value is also removed, so unadaptable inputs raise instead of leaking unescaped values.\n\n 3. Regression tests: a per-template behavioural test renders each previously-vulnerable template with an apostrophe-injection payload and asserts the escaped fragment is present and the vulnerable fragment absent; a lint test walks every ``*.sql`` template flagging any ``\u0027{{ ... }}\u0027`` single-quote-wrapped interpolation against an explicit allowlist; unit tests cover the new qtLiteral fail-fast and inner-except raise paths.\n\nThis issue affects pgAdmin 4: from 1.0 before 9.16."
},
{
"lang": "es",
"value": "Inyecci\u00f3n SQL en pgAdmin 4 en cada plantilla de di\u00e1logo que renderiza \u201cCOMMENT ON ... IS \u2019[description]\u2019\u201d para un campo de descripci\u00f3n proporcionado por el usuario. Las plantillas Jinja para Dominios (y sus restricciones), Tablas For\u00e1neas, Lenguajes y Disparadores de Eventos, adem\u00e1s de la consulta de b\u00fasqueda de OID de Vistas, interpolaron la descripci\u00f3n directamente dentro de un literal SQL entre comillas simples - \u201c\u2019{{ data.description }}\u2019\u201d - en lugar de pasarla a trav\u00e9s del filtro de escape \u201cqtLiteral\u201d. Un usuario autenticado de pgAdmin con permiso para crear o alterar los tipos de objetos afectados podr\u00eda enviar una descripci\u00f3n que contenga un ap\u00f3strofe, salir del literal y encadenar SQL arbitrario. El SQL inyectado se ejecuta bajo el rol de PostgreSQL con el que el usuario ya est\u00e1 autenticado; para un rol conectado con \u201cCOPY ... TO/FROM PROGRAM\u201d (t\u00edpicamente superusuario de PostgreSQL), esto se encadena a la ejecuci\u00f3n de comandos del sistema operativo en el host de PostgreSQL. El defecto no cruza un l\u00edmite de privilegios - el usuario ya tiene acceso SQL directo a ese rol a trav\u00e9s de la Herramienta de Consulta de pgAdmin - por lo que el atacante no obtiene ninguna capacidad m\u00e1s all\u00e1 de lo que su rol de base de datos ya le otorga. El impacto marginal captura la omisi\u00f3n de cualquier control de la Herramienta de Consulta a nivel de aplicaci\u00f3n que un operador pueda haber configurado.\n\nEl defecto fue reportado originalmente contra el campo \u201cdescription\u201d del Di\u00e1logo de Dominio; una auditor\u00eda de c\u00f3digo a nivel general identific\u00f3 diecis\u00e9is sitios del mismo patr\u00f3n en las plantillas mencionadas anteriormente. La misma revisi\u00f3n tambi\u00e9n revel\u00f3 diez sumideros relacionados en las plantillas de estad\u00edsticas pgstattuple/pgstatindex - \u201cpgstattuple(\u2019{{schema}}.{{table}}\u2019)\u201d y la forma coincidente de pgstatindex - donde \u201cqtIdent\u201d escapa las comillas dobles incrustadas dentro del identificador pero no los ap\u00f3strofes, por lo que un usuario con privilegio CREATE en un esquema podr\u00eda \u201cplantar\u201d una tabla o \u00edndice llamado \u201cfoo\u2019bar\u201d y un visor de estad\u00edsticas posterior renderizar\u00eda un literal desequilibrado.\n\nLa soluci\u00f3n est\u00e1 en capas:\n\n1. Sitios: reemplazar cada \u201c\u2019{{ x.description }}\u2019\u201d con \u201c{{ x.description|qtLiteral(conn) }}\u201d (sin comillas circundantes - el filtro envuelve el valor en comillas escapadas por s\u00ed mismo). Conectar \u201cconn=self.conn\u201d a trav\u00e9s de cada llamada a \u201crender_template\u201d que carga una de estas plantillas. Tambi\u00e9n corrige un error tipogr\u00e1fico de Jinja \u201c{ % elif\u201d en la diferencia de esquema de tabla for\u00e1nea (rama muerta). Reescribir los diez sitios de estad\u00edsticas pgstattuple/pgstatindex para abordar la relaci\u00f3n a trav\u00e9s de OID + conversi\u00f3n \u201c::oid::regclass\u201d (p. ej., \u201cpgstattuple({{ tid }}::oid::regclass)\u201d), eliminando por completo la forma de llamada literal incrustada para que esa clase de error ya no pueda repetirse all\u00ed.\n\n2. Reforzamiento del controlador: \u201cqtLiteral\u201d (en \u201cutils/driver/psycopg3/__init__.py\u201d) sol\u00eda devolver silenciosamente el valor sin escapar en bruto cuando su argumento \u201cconn\u201d era falso. Ahora lanza \u201cValueError\u201d - sacando a la luz toda la clase de errores en el futuro. El cambio descubri\u00f3 inmediatamente ocho errores de conexi\u00f3n latentes (en \u201cschemas/__init__.py\u201d, \u201cschemas/functions/__init__.py\u201d, \u201cschemas/tables/utils.py\u201d, \u201cforeign_servers/__init__.py\u201d, y siete sitios en \u201croles/__init__.py\u201d) - todos corregidos como parte de este parche. Tambi\u00e9n se elimina el bloque \u201cexcept\u201d interno que \u201ctragaba\u201d los fallos a nivel de adaptador y devolv\u00eda el valor en bruto, por lo que las entradas no adaptables lanzan una excepci\u00f3n en lugar de filtrar valores sin escapar.\n\n3. Pruebas de regresi\u00f3n: una prueba de comportamiento por plantilla renderiza cada plantilla previamente vulnerable con una carga \u00fatil de inyecci\u00f3n de ap\u00f3strofe y afirma que el fragmento escapado est\u00e1 presente y el fragmento vulnerable ausente; una prueba de lint recorre cada plantilla \u201c*.sql\u201d marcando cualquier interpolaci\u00f3n \u201c\u2019{{ ... }}\u2019\u201d envuelta en comillas simples contra una lista de permitidos expl\u00edcita; las pruebas --TRUNCADO--"
}
],
"id": "CVE-2026-12044",
"lastModified": "2026-10-26T11:10:00.157",
"metrics": {
"cvssMetricV31": [
{
"cvssData": {
"attackComplexity": "LOW",
"attackVector": "NETWORK",
"availabilityImpact": "HIGH",
"baseScore": 8.8,
"baseSeverity": "HIGH",
"confidentialityImpact": "HIGH",
"integrityImpact": "HIGH",
"privilegesRequired": "LOW",
"scope": "UNCHANGED",
"userInteraction": "NONE",
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
"version": "3.1"
},
"exploitabilityScore": 2.8,
"impactScore": 5.9,
"source": "f86ef6dc-4d3a-42ad-8f28-e6d5547a5007",
"type": "Secondary"
}
],
"cvssMetricV40": [
{
"cvssData": {
"Automatable": "NOT_DEFINED",
"Recovery": "NOT_DEFINED",
"Safety": "NOT_DEFINED",
"attackComplexity": "LOW",
"attackRequirements": "NONE",
"attackVector": "NETWORK",
"availabilityRequirement": "NOT_DEFINED",
"baseScore": 8.7,
"baseSeverity": "HIGH",
"confidentialityRequirement": "NOT_DEFINED",
"exploitMaturity": "NOT_DEFINED",
"integrityRequirement": "NOT_DEFINED",
"modifiedAttackComplexity": "NOT_DEFINED",
"modifiedAttackRequirements": "NOT_DEFINED",
"modifiedAttackVector": "NOT_DEFINED",
"modifiedPrivilegesRequired": "NOT_DEFINED",
"modifiedSubAvailabilityImpact": "NOT_DEFINED",
"modifiedSubConfidentialityImpact": "NOT_DEFINED",
"modifiedSubIntegrityImpact": "NOT_DEFINED",
"modifiedUserInteraction": "NOT_DEFINED",
"modifiedVulnAvailabilityImpact": "NOT_DEFINED",
"modifiedVulnConfidentialityImpact": "NOT_DEFINED",
"modifiedVulnIntegrityImpact": "NOT_DEFINED",
"privilegesRequired": "LOW",
"providerUrgency": "NOT_DEFINED",
"subAvailabilityImpact": "NONE",
"subConfidentialityImpact": "NONE",
"subIntegrityImpact": "NONE",
"userInteraction": "NONE",
"valueDensity": "NOT_DEFINED",
"vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"version": "4.0",
"vulnAvailabilityImpact": "HIGH",
"vulnConfidentialityImpact": "HIGH",
"vulnIntegrityImpact": "HIGH",
"vulnerabilityResponseEffort": "NOT_DEFINED"
},
"source": "f86ef6dc-4d3a-42ad-8f28-e6d5547a5007",
"type": "Secondary"
}
],
"ssvcV203": [
{
"source": "134c704f-9b21-4f2e-91b3-4a467353bcc0",
"ssvcData": {
"id": "CVE-2026-12044",
"options": [
{
"exploitation": "none"
},
{
"automatable": "no"
},
{
"technicalImpact": "total"
}
],
"role": "CISA Coordinator",
"timestamp": "2026-06-22T18:36:59.059744Z",
"version": "2.0.3"
}
}
]
},
"published": "2026-06-19T00:16:46.053",
"references": [
{
"source": "f86ef6dc-4d3a-42ad-8f28-e6d5547a5007",
"tags": [
"Patch"
],
"url": "https://github.com/pgadmin-org/pgadmin4/commit/2ae0d3610"
},
{
"source": "f86ef6dc-4d3a-42ad-8f28-e6d5547a5007",
"tags": [
"Patch"
],
"url": "https://github.com/pgadmin-org/pgadmin4/commit/658bb585d"
},
{
"source": "f86ef6dc-4d3a-42ad-8f28-e6d5547a5007",
"tags": [
"Issue Tracking",
"Patch",
"Third Party Advisory"
],
"url": "https://github.com/pgadmin-org/pgadmin4/issues/10078"
}
],
"sourceIdentifier": "f86ef6dc-4d3a-42ad-8f28-e6d5547a5007",
"vulnStatus": "Analyzed",
"weaknesses": [
{
"description": [
{
"lang": "en",
"value": "CWE-89"
},
{
"lang": "en",
"value": "CWE-116"
}
],
"source": "f86ef6dc-4d3a-42ad-8f28-e6d5547a5007",
"type": "Secondary"
}
]
}
Loading…
Loading…
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Experimental. This forecast is provided for visualization only and may change without notice. Do not use it for operational decisions.
Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
Loading…
Loading…
The MITRE ATT&CK techniques below are AI-generated suggestions, inferred from the description of the
vulnerability by the CIRCL/vulnerability-attack-technique-classification-roberta-base
model, served locally by ML-Gateway.
They have not been verified by an analyst and are provided for guidance only.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Loading…
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.