← Back to CVE Explorer

CVE-2026-12048

Full vulnerability record from the local CVENow database.

Description

Stored cross-site scripting in pgAdmin 4's error-rendering and plan-node-rendering paths. Text returned by a PostgreSQL server (ErrorResponse messages, including object names quoted back inside relation-does-not-exist errors and inside EXPLAIN Recheck Cond / Exact Heap Blocks fields) was passed verbatim through html-react-parser at every user-facing sink — the notifier toasts, FormFooterMessage / FormInput help and error areas, FormNote, ModalProvider AlertContent and confirmDelete, ToolErrorView, the Explain visualiser's NodeText panel, the SQL editor confirm dialogs, ConfirmSaveContent, PreferencesHelper modal alerts, and SelectThemes helper text. A PostgreSQL server an attacker controls — or any server returning attacker-influenced text such as a table or column name a low-privilege database user can create — could inject arbitrary HTML (including <iframe>) into the pgAdmin DOM the moment the victim's pgAdmin connected to that server or viewed an Explain plan that referenced the crafted object. The injected iframe's srcdoc could fetch attacker-served JavaScript and, by writing to parent.location, redirect the victim's top-level pgAdmin browser tab to an attacker-controlled URL. Because the injection originates from inside pgAdmin's own interface, standard anti-clickjacking controls (X-Frame-Options, Content-Security-Policy: frame-ancestors) do not mitigate it. A phishing page rendered inside the legitimate pgAdmin window is indistinguishable from a genuine pgAdmin dialog. Fix combines three complementary layers. (1) DOMPurify sanitisation is wrapped around every html-react-parser call site reachable from notifier, alert, form-error, Explain, and SQL-editor flows. (2) A new plain-text rendering contract — SafeMessage / SafeHtmlMessage components plus Notifier.errorText / alertText / warningText / infoText / successText helpers — is introduced; around fifty callers across browser, tools, dashboard, debugger, misc, llm, preferences, schema diff, and the SQL editor that previously interpolated backend-derived strings are migrated to the plain-text variants. (3) Backend HTML-escape is applied at the post-connection-SQL handler (execute_post_connection_sql) via a new sanitize_external_text helper, so third-party JSON consumers (audit logs, API clients) never receive raw markup either; the Explain plan-info renderer is also patched to _.escape Recheck Cond and Exact Heap Blocks at construction (matching every sibling field), giving defence in depth even before DOMPurify runs. This issue affects pgAdmin 4: from 6.0 before 9.16.

Severity
CRITICAL
CVSS score
9.3 (v4.0)
Vulnerability status
Analyzed
Source identifier
f86ef6dc-4d3a-42ad-8f28-e6d5547a5007
Published
2026-06-19 00:16:47.200000
Last modified
2026-10-26 11:10:00.157000
View original CVE JSON
{
  "id": "CVE-2026-12048",
  "sourceIdentifier": "f86ef6dc-4d3a-42ad-8f28-e6d5547a5007",
  "published": "2026-06-19T00:16:47.200",
  "lastModified": "2026-10-26T11:10:00.157",
  "vulnStatus": "Analyzed",
  "cveTags": [],
  "descriptions": [
    {
      "lang": "en",
      "value": "Stored cross-site scripting in pgAdmin 4's error-rendering and plan-node-rendering paths. Text returned by a PostgreSQL server (ErrorResponse messages, including object names quoted back inside relation-does-not-exist errors and inside EXPLAIN Recheck Cond / Exact Heap Blocks fields) was passed verbatim through html-react-parser at every user-facing sink — the notifier toasts, FormFooterMessage / FormInput help and error areas, FormNote, ModalProvider AlertContent and confirmDelete, ToolErrorView, the Explain visualiser's NodeText panel, the SQL editor confirm dialogs, ConfirmSaveContent, PreferencesHelper modal alerts, and SelectThemes helper text. A PostgreSQL server an attacker controls — or any server returning attacker-influenced text such as a table or column name a low-privilege database user can create — could inject arbitrary HTML (including <iframe>) into the pgAdmin DOM the moment the victim's pgAdmin connected to that server or viewed an Explain plan that referenced the crafted object.\n\nThe injected iframe's srcdoc could fetch attacker-served JavaScript and, by writing to parent.location, redirect the victim's top-level pgAdmin browser tab to an attacker-controlled URL. Because the injection originates from inside pgAdmin's own interface, standard anti-clickjacking controls (X-Frame-Options, Content-Security-Policy: frame-ancestors) do not mitigate it. A phishing page rendered inside the legitimate pgAdmin window is indistinguishable from a genuine pgAdmin dialog.\n\nFix combines three complementary layers. (1) DOMPurify sanitisation is wrapped around every html-react-parser call site reachable from notifier, alert, form-error, Explain, and SQL-editor flows. (2) A new plain-text rendering contract — SafeMessage / SafeHtmlMessage components plus Notifier.errorText / alertText / warningText / infoText / successText helpers — is introduced; around fifty callers across browser, tools, dashboard, debugger, misc, llm, preferences, schema diff, and the SQL editor that previously interpolated backend-derived strings are migrated to the plain-text variants. (3) Backend HTML-escape is applied at the post-connection-SQL handler (execute_post_connection_sql) via a new sanitize_external_text helper, so third-party JSON consumers (audit logs, API clients) never receive raw markup either; the Explain plan-info renderer is also patched to _.escape Recheck Cond and Exact Heap Blocks at construction (matching every sibling field), giving defence in depth even before DOMPurify runs.\n\nThis issue affects pgAdmin 4: from 6.0 before 9.16."
    },
    {
      "lang": "es",
      "value": "Cross-site scripting almacenado en las rutas de renderizado de errores y renderizado de nodos de plan de pgAdmin 4. El texto devuelto por un servidor PostgreSQL (mensajes ErrorResponse, incluyendo nombres de objetos citados dentro de errores ’relation-does-not-exist’ y dentro de campos EXPLAIN ’Recheck Cond’ / ’Exact Heap Blocks’) se pasó textualmente a través de html-react-parser en cada punto de consumo orientado al usuario - los ’toasts’ del notificador, las áreas de ayuda y error de FormFooterMessage / FormInput, FormNote, ModalProvider AlertContent y confirmDelete, ToolErrorView, el panel NodeText del visualizador de Explain, los diálogos de confirmación del editor SQL, ConfirmSaveContent, las alertas modales de PreferencesHelper y el texto de ayuda de SelectThemes. Un servidor PostgreSQL que un atacante controla - o cualquier servidor que devuelva texto influenciado por el atacante, como el nombre de una tabla o columna que un usuario de base de datos con pocos privilegios puede crear - podría inyectar HTML arbitrario (incluyendo [iframe_tag]) en el DOM de pgAdmin en el momento en que el pgAdmin de la víctima se conectara a ese servidor o viera un plan Explain que hiciera referencia al objeto manipulado.\n\nEl srcdoc del iframe inyectado podría obtener JavaScript servido por el atacante y, escribiendo en parent.location, redirigir la pestaña del navegador pgAdmin de nivel superior de la víctima a una URL controlada por el atacante. Debido a que la inyección se origina desde dentro de la propia interfaz de pgAdmin, los controles estándar anti-clickjacking (X-Frame-Options, Content-Security-Policy: frame-ancestors) no lo mitigan. Una página de phishing renderizada dentro de la ventana legítima de pgAdmin es indistinguible de un diálogo genuino de pgAdmin.\n\nLa solución combina tres capas complementarias. (1) La sanitización de DOMPurify se aplica alrededor de cada sitio de llamada de html-react-parser accesible desde los flujos de notificador, alerta, error de formulario, Explain y editor SQL. (2) Se introduce un nuevo contrato de renderizado de texto plano - componentes SafeMessage / SafeHtmlMessage más los ayudantes Notifier.errorText / alertText / warningText / infoText / successText; alrededor de cincuenta llamadores en el navegador, herramientas, panel de control, depurador, miscelánea, llm, preferencias, diferencia de esquema y el editor SQL que previamente interpolaban cadenas derivadas del backend se migran a las variantes de texto plano. (3) El escape HTML del backend se aplica en el gestor post-conexión-SQL (execute_post_connection_sql) a través de un nuevo ayudante sanitize_external_text, para que los consumidores de JSON de terceros (registros de auditoría, clientes API) tampoco reciban marcado sin procesar; el renderizador de información del plan Explain también se parchea para _.escape Recheck Cond y Exact Heap Blocks en la construcción (coincidiendo con cada campo hermano), proporcionando defensa en profundidad incluso antes de que se ejecute DOMPurify.\n\nEste problema afecta a pgAdmin 4: desde la versión 6.0 anterior a la 9.16."
    }
  ],
  "affected": [
    {
      "source": "f86ef6dc-4d3a-42ad-8f28-e6d5547a5007",
      "affectedData": [
        {
          "vendor": "pgadmin.org",
          "product": "pgAdmin 4",
          "defaultStatus": "unaffected",
          "modules": [
            "Notifier",
            "Form Components",
            "Modal Provider",
            "Explain Visualiser",
            "Driver"
          ],
          "programFiles": [
            "https://github.com/pgadmin-org/pgadmin4/blob/master/web/pgadmin/static/js/components/FormComponents.jsx",
            "https://github.com/pgadmin-org/pgadmin4/blob/master/web/pgadmin/static/js/Notifier.jsx",
            "https://github.com/pgadmin-org/pgadmin4/blob/master/web/pgadmin/utils/driver/psycopg3/connection.py",
            "https://github.com/pgadmin-org/pgadmin4/blob/master/web/pgadmin/tools/erd/static/js/erd_tool/components/Explain/Analysis.jsx"
          ],
          "repo": "https://github.com/pgadmin-org/pgadmin4",
          "versions": [
            {
              "version": "6.0",
              "lessThan": "9.16",
              "versionType": "custom",
              "status": "affected"
            }
          ]
        }
      ]
    }
  ],
  "metrics": {
    "cvssMetricV40": [
      {
        "source": "f86ef6dc-4d3a-42ad-8f28-e6d5547a5007",
        "type": "Secondary",
        "cvssData": {
          "version": "4.0",
          "vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:N/SC:H/SI:H/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",
          "baseScore": 9.3,
          "baseSeverity": "CRITICAL",
          "attackVector": "NETWORK",
          "attackComplexity": "LOW",
          "attackRequirements": "NONE",
          "privilegesRequired": "NONE",
          "userInteraction": "ACTIVE",
          "vulnConfidentialityImpact": "HIGH",
          "vulnIntegrityImpact": "HIGH",
          "vulnAvailabilityImpact": "NONE",
          "subConfidentialityImpact": "HIGH",
          "subIntegrityImpact": "HIGH",
          "subAvailabilityImpact": "NONE",
          "exploitMaturity": "NOT_DEFINED",
          "confidentialityRequirement": "NOT_DEFINED",
          "integrityRequirement": "NOT_DEFINED",
          "availabilityRequirement": "NOT_DEFINED",
          "modifiedAttackVector": "NOT_DEFINED",
          "modifiedAttackComplexity": "NOT_DEFINED",
          "modifiedAttackRequirements": "NOT_DEFINED",
          "modifiedPrivilegesRequired": "NOT_DEFINED",
          "modifiedUserInteraction": "NOT_DEFINED",
          "modifiedVulnConfidentialityImpact": "NOT_DEFINED",
          "modifiedVulnIntegrityImpact": "NOT_DEFINED",
          "modifiedVulnAvailabilityImpact": "NOT_DEFINED",
          "modifiedSubConfidentialityImpact": "NOT_DEFINED",
          "modifiedSubIntegrityImpact": "NOT_DEFINED",
          "modifiedSubAvailabilityImpact": "NOT_DEFINED",
          "Safety": "NOT_DEFINED",
          "Automatable": "NOT_DEFINED",
          "Recovery": "NOT_DEFINED",
          "valueDensity": "NOT_DEFINED",
          "vulnerabilityResponseEffort": "NOT_DEFINED",
          "providerUrgency": "NOT_DEFINED"
        }
      }
    ],
    "cvssMetricV31": [
      {
        "source": "f86ef6dc-4d3a-42ad-8f28-e6d5547a5007",
        "type": "Secondary",
        "cvssData": {
          "version": "3.1",
          "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N",
          "baseScore": 9.3,
          "baseSeverity": "CRITICAL",
          "attackVector": "NETWORK",
          "attackComplexity": "LOW",
          "privilegesRequired": "NONE",
          "userInteraction": "REQUIRED",
          "scope": "CHANGED",
          "confidentialityImpact": "HIGH",
          "integrityImpact": "HIGH",
          "availabilityImpact": "NONE"
        },
        "exploitabilityScore": 2.8,
        "impactScore": 5.8
      },
      {
        "source": "[email protected]",
        "type": "Primary",
        "cvssData": {
          "version": "3.1",
          "vectorString": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N",
          "baseScore": 5.4,
          "baseSeverity": "MEDIUM",
          "attackVector": "NETWORK",
          "attackComplexity": "LOW",
          "privilegesRequired": "LOW",
          "userInteraction": "REQUIRED",
          "scope": "CHANGED",
          "confidentialityImpact": "LOW",
          "integrityImpact": "LOW",
          "availabilityImpact": "NONE"
        },
        "exploitabilityScore": 2.3,
        "impactScore": 2.7
      }
    ],
    "ssvcV203": [
      {
        "source": "134c704f-9b21-4f2e-91b3-4a467353bcc0",
        "ssvcData": {
          "timestamp": "2026-06-22T19:15:27.799712Z",
          "id": "CVE-2026-12048",
          "options": [
            {
              "exploitation": "none"
            },
            {
              "automatable": "no"
            },
            {
              "technicalImpact": "total"
            }
          ],
          "role": "CISA Coordinator",
          "version": "2.0.3"
        }
      }
    ]
  },
  "weaknesses": [
    {
      "source": "f86ef6dc-4d3a-42ad-8f28-e6d5547a5007",
      "type": "Secondary",
      "description": [
        {
          "lang": "en",
          "value": "CWE-79"
        },
        {
          "lang": "en",
          "value": "CWE-116"
        }
      ]
    },
    {
      "source": "[email protected]",
      "type": "Primary",
      "description": [
        {
          "lang": "en",
          "value": "CWE-79"
        }
      ]
    }
  ],
  "configurations": [
    {
      "nodes": [
        {
          "operator": "OR",
          "negate": false,
          "cpeMatch": [
            {
              "vulnerable": true,
              "criteria": "cpe:2.3:a:pgadmin:pgadmin_4:*:*:*:*:*:postgresql:*:*",
              "versionStartIncluding": "6.0",
              "versionEndExcluding": "9.16",
              "matchCriteriaId": "E0595842-CDC3-4CF7-B5EA-DF325B7E784A"
            }
          ]
        }
      ]
    }
  ],
  "references": [
    {
      "url": "https://github.com/pgadmin-org/pgadmin4/commit/9e370d3cb67b83b3945f82969c959fad3f926517",
      "source": "f86ef6dc-4d3a-42ad-8f28-e6d5547a5007",
      "tags": [
        "Patch"
      ]
    },
    {
      "url": "https://github.com/pgadmin-org/pgadmin4/issues/10068",
      "source": "f86ef6dc-4d3a-42ad-8f28-e6d5547a5007",
      "tags": [
        "Issue Tracking",
        "Patch"
      ]
    }
  ]
}