# CloudNativePG's metrics exporter allows privilege escalation to PostgreSQL superuser and OS RCE

CloudNativePG metrics exporter connects as postgres superuser then SET ROLE pg_monitor; a low-privileged DB owner shadows an unqualified identifier to RESET ROLE and gain superuser plus OS RCE.

- Payout: Research
- Severity: critical
- Program: CloudNativePG
- Found by: mdisec
- Disclosed: 2026-05-11
- CVE: CVE-2026-44477
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-423p-g724-fr39
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-423p-g724-fr39
- Tags: area:cloud domain:cloud domain:infra impact:privesc impact:rce kind:research platform:ghsa sev:critical tech:kubernetes technique:chained technique:source-review vuln:privilege-escalation vuln:rce

**Target:** PostgreSQL metrics exporter scrape session (Unix socket connection)
**Root cause:** Exporter authenticates as the postgres superuser and only demotes with SET ROLE pg_monitor, which changes current_user but leaves session_user=postgres. Any SQL expression evaluated in the scrape session can RESET ROLE and regain superuser.
**Steps:**
1. Identify a scraped database where the attacker owns a schema on the exporter's search_path (or owns the database, gaining public via pg_database_owner; on PG<15 public grants CREATE to PUBLIC).
2. Plant a shadow object matching an unqualified identifier used by a scraped metric query, e.g. shadow `current_database()` used by the stock `pg_extensions` metric with `target_databases: '*'`.
3. Wait for the next scrape (≤30s); the shadow expression executes with session_user=postgres.
4. Inside the expression run `RESET ROLE` to recover real superuser privileges.
5. Use `COPY ... TO PROGRAM` to spawn an OS subprocess as the postgres user in the primary pod; READ ONLY transaction flag does not block it.
**How found:** Noticed the exporter authenticates as postgres and only demotes via SET ROLE, leaving session_user=postgres; realized any expression in the scrape session is a superuser code path, then found the stock default-monitoring.yaml used unqualified current_database() against target_databases '*'.
**Exploitation:** Attacker with a low-privileged DB role (e.g. default `app`) plants a shadow function/relation on the exporter's search_path; on the next scrape the metric query resolves to the attacker object, which executes as session_user=postgres, RESET ROLE restores superuser, and COPY ... TO PROGRAM yields OS command execution in the primary pod. A SQL injection in an app backed by CNPG suffices to pivot to pod RCE.
**Escalation:** Chains a low-privileged DB role to PostgreSQL superuser to OS command execution inside the primary pod; SQLi in a CNPG-backed app becomes pod RCE.
**Insight:** SET ROLE / SET SESSION AUTHORIZATION are not real privilege boundaries: session_user and the authenticated identity persist, so RESET ROLE / RESET SESSION AUTHORIZATION recover the original. Audit any exporter or sidecar that connects as a superuser and demotes in SQL; also grep shipped monitoring configs for unqualified catalog calls on a user-writable search_path.

