{"id":"CVE-2026-44477","aliases":["GHSA-423p-g724-fr39","GO-2026-5106"],"title":"CloudNativePG's metrics exporter allows privilege escalation to PostgreSQL superuser and OS RCE","summary":"CloudNativePG's metrics exporter allows privilege escalation to PostgreSQL superuser and OS RCE","severity":"critical","cvss":9.9,"cvssVector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H","vendor":"cloudnative-pg","product":"github.com/cloudnative-pg/cloudnative-pg","ecosystem":"go","affected":["github.com/cloudnative-pg/cloudnative-pg < 1.28.3","github.com/cloudnative-pg/cloudnative-pg >= 1.29.0, < 1.29.1"],"patched":["github.com/cloudnative-pg/cloudnative-pg 1.28.3","github.com/cloudnative-pg/cloudnative-pg 1.29.1"],"published":"2026-05-11","updated":"2026-09-10","sourceUpdated":"2026-09-10T03:50:45.436330963Z","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-423p-g724-fr39","references":[{"url":"https://github.com/cloudnative-pg/cloudnative-pg/security/advisories/GHSA-423p-g724-fr39"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-44477"},{"url":"https://github.com/cloudnative-pg/cloudnative-pg/pull/10576"},{"url":"https://github.com/cloudnative-pg/cloudnative-pg"},{"url":"https://github.com/cloudnative-pg/cloudnative-pg/releases/tag/v1.28.3"},{"url":"https://github.com/cloudnative-pg/cloudnative-pg/releases/tag/v1.29.1"}],"tags":["osv","go"],"epss":0.0048,"epssPercentile":0.4057,"ingestedAt":"2026-09-12T03:13:01.750Z","slug":"CVE-2026-44477","body":"## Overview\n\n### Impact\n\nThe CloudNativePG metrics exporter opens its PostgreSQL connection as the `postgres` superuser via the pod-local Unix socket, then demotes the session with `SET ROLE pg_monitor`. `SET ROLE` changes only `current_user`; `session_user` remains `postgres`. That residual superuser identity is the foothold for the rest of the chain.\n\nAny SQL expression evaluated inside the scrape session can invoke `RESET ROLE` to recover real superuser privileges, then use `COPY ... TO PROGRAM` to spawn an OS-level subprocess as the `postgres` user inside the primary pod. The `READ ONLY` transaction flag does not block this; it gates writes to database state, not external processes.\n\nTwo exploitation paths follow from this root cause.\n\n#### Path 1: custom metric queries with unqualified identifiers (all supported releases)\n\nA database user who owns a schema on the `search_path` of any scraped database can plant a shadow object whose name matches an unqualified identifier in a custom metric query. When the exporter next evaluates that query, the shadow expression executes inside the `session_user = postgres` scrape session, giving the attacker PostgreSQL superuser privileges and OS command execution inside the primary pod within one scrape interval (≤30 s). Exploitability requires a custom metric query that contains an unqualified relation or function reference.\n\nAlthough `search_path` shadowing of unqualified identifiers is the most direct case, the underlying bug is that any expression evaluated inside the scrape session is a superuser code path. Other exploitable shapes include user-defined functions, operators or casts resolved during the scrape, joins or subqueries against user-owned tables and views, and index expressions or RLS policies on read-touched objects.\n\n#### Path 2: stock `default-monitoring.yaml` (all supported releases, no custom metrics required)\n\nThe `pg_extensions` metric shipped in `default-monitoring.yaml` used an unqualified `current_database()` call and ran against every user database (`target_databases: '*'`). Any non-superuser who owns a user database (including the default `app` role created by `bootstrap.initdb`) could shadow `current_database()` and trigger the full escalation chain against a stock CNPG deployment on the first scrape after the shadow was planted.\n\n#### Combined impact\n\nThe chain yields privilege escalation from a low-privileged database role (e.g. the default `app` role) to PostgreSQL superuser, plus arbitrary OS command execution as the `postgres` user inside the primary pod, all within one scrape interval. A web application SQL injection vulnerability in an app backed by a CNPG cluster is therefore sufficient to pivot to database-pod RCE.\n\n#### Who is impacted\n\n- All deployments on any supported release with default monitoring enabled are affected by Path 2.\n- All deployments on any supported release that use custom metric queries containing unqualified catalog references are affected by Path 1.\n- Multi-tenant platforms that allow customers to supply or influence custom metric query bodies are at the highest risk for Path 1.\n\n### Patches\n\nThree separate patches address the vulnerability.\n\n#### Patch 1: PR #10576 \"schema-qualify catalog references in default monitoring queries and documentation samples\"\n\nSchema-qualifies all unqualified `pg_catalog` function and view references in the shipped `default-monitoring.yaml` and in documentation examples. This closes Path 2 in operator-shipped configuration and removes the unqualified-identifier attack surface from all operator-shipped metric queries. Operators who clone or copy `default-monitoring.yaml` into custom monitoring `ConfigMap`s, or have copy-pasted unqualified queries elsewhere, must re-qualify those queries themselves.\n\nBackported to all currently supported releases:\n\n- **v1.29.x** (x ≥ 1)\n- **v1.28.x** (x ≥ 3)\n\n#### Patch 2: \"dedicated `cnpg_metrics_exporter` role with `pg_ident.conf` peer mapping\"\n\nIntroduces a dedicated `cnpg_metrics_exporter` PostgreSQL role (granted `pg_monitor`, no superuser privileges) and maps it in `pg_ident.conf` via peer authentication on the local Unix socket, following the same pattern already used for `cnpg_pooler_pgbouncer`. The metrics exporter connects as this role instead of `postgres`, so `session_user` is never a superuser and `RESET ROLE` has no escalation effect. This eliminates the root cause entirely.\n\nDemoting the session at the SQL level (via `SET SESSION AUTHORIZATION pg_monitor`) is not sufficient: the privilege check for `SET SESSION AUTHORIZATION` is whether the *authenticated* user is a superuser, not the current `session_user`. With the connection still authenticated as `postgres`, any SQL in the session can run `RESET SESSION AUTHORIZATION` and recover the original superuser identity. This is the same recovery primitive as `RESET ROLE`, one layer up. Only changing the authenticated user closes the loop.\n\nWith this change in place, the original chain breaks at every step: `RESET ROLE` and `RESET SESSION AUTHORIZATION` cannot recover superuser, and `COPY ... TO PROGRAM` requires a privilege `pg_monitor` does not grant. As defense in depth, the monitoring transaction also prepends `pg_catalog` to the connection's `search_path`, so unqualified catalog identifiers cannot resolve to user-planted shadow objects.\n\nThis patch changes the connection identity but not how queries are evaluated. Custom metric queries within `pg_monitor`'s scope (catalog reads, `pg_stat_*` views, settings) continue to work without modification. Queries that previously relied on superuser-level access (reading user-owned tables not granted to `cnpg_metrics_exporter`, or superuser-only catalogs such as `pg_authid` or `pg_subscription`) will fail and need explicit `GRANT` statements to `cnpg_metrics_exporter`.\n\nThe role is created and maintained with `PASSWORD NULL`; any password set out-of-band is cleared on the next reconcile, so the role cannot be authenticated by password regardless of operator pre-creation.\n\nFor replica clusters, upgrade the source primary cluster before any replica clusters that consume from it. The `cnpg_metrics_exporter` role is created on the source primary and replicates downstream; a replica cluster upgraded first will scrape against a missing role until the source primary upgrades or the role is created manually (see the monitoring documentation).\n\nThe patch will be backported to all currently supported releases:\n\n- **v1.29.x** (x ≥ 1)\n- **v1.28.x** (x ≥ 3)\n\n### Workarounds\n\nIf upgrading immediately is not possible:\n\n1. **Schema-qualify all identifiers in custom metric queries.** Use explicit `pg_catalog.` prefixes for all catalog functions and views (e.g. `pg_catalog.current_database()`, `pg_catalog.now()`). This is a partial mitigation: it closes the `search_path`-shadowing shape in operator- and user-supplied metric bodies, but other expression shapes (user-defined functions, operators or casts; joins or subqueries on user-owned tables and views; RLS policies on read-touched objects) remain superuser code paths until Patch 2 lands.\n\n2. **Restrict database ownership.** Ensure only fully trusted roles own user databases in scraped clusters. The exploit requires the ability to plant an object on the metrics exporter's `search_path` in a scraped database, typically by owning the database (and therefore `public` via `pg_database_owner`) or by holding `CREATE` on a schema already reachable through `search_path`.\n\n    *PG <15 caveat:* `public` grants `CREATE` to `PUBLIC` by default before PostgreSQL 15, so any authenticated role in a scraped database can plant a shadow object regardless of ownership.\n\n3. **Limit the scope of `target_databases: '*'` queries.** Avoid `target_databases: '*'` unless every database in the cluster, and every role that owns one, is fully trusted. Where possible, restrict `target_databases` to specific, known-safe databases.\n\n4. **Do not expose metric query SQL to untrusted users.** Multi-tenant platforms that allow customers to supply or influence custom metric query bodies should treat this as a critical trust boundary until the architectural fix is released.\n\n### References\n\n- Fix (Patch 1): PR #10576 \"schema-qualify catalog references in default monitoring queries and documentation samples\"\n- Fix (Patch 2): \"dedicated `cnpg_metrics_exporter` role with `pg_ident.conf` peer mapping\"\n- Reported by: Mehmet Ince\n\n## Affected packages\n\n- `github.com/cloudnative-pg/cloudnative-pg < 1.28.3`\n- `github.com/cloudnative-pg/cloudnative-pg >= 1.29.0, < 1.29.1`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `github.com/cloudnative-pg/cloudnative-pg 1.28.3`\n- `github.com/cloudnative-pg/cloudnative-pg 1.29.1`","depth":"midnight","depthScore":55,"depthScoreParts":{"impact":54.5,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}