Skip to content

CVE-2026-19633: Verified Reproduction

CVE-2026-19633: PostgreSQL Anonymizer ≤3.1.3 arbitrary code execution as extension superuser via crafted masking constructs

CVE-2026-19633 is verified against the affected target. Vulnerability class: Privilege Escalation. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00362.

REPRO-2026-00362 Privilege Escalation Sep 24, 2026 CVE entry .txt
Severity
HIGH
Confidence
HIGH
Reproduced in
72m 52s
Tool calls
355
Spend
$6.53
01 · Overview

What Is CVE-2026-19633?

CVE-2026-19633 is a high-severity Privilege Escalation vulnerability. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00362).

02 · Severity & CVSS

CVE-2026-19633 Severity

CVE-2026-19633 is rated high severity.

HIGH threat level
Weakness CWE-94 — Improper Control of Generation of Code ('Code Injection')

High — serious impact or readily exploitable. Prioritize remediation.

How to Reproduce CVE-2026-19633

$ pruva-verify REPRO-2026-00362
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00362/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-19633

Remote code execution — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

SQL statements from an unprivileged LOGIN role: masking rules (SECURITY LABEL FOR anon ... MASKED WITH FUNCTION ...) embedding a custom operator, a domain cast and a view subquery that hide attacker-defined plpgsql payloads

Attack chain
  1. unprivileged role declares masking rules
  2. extension superuser runs anon.anonymize_table() (static masking)
  3. masking expressions evaluated in superuser security context
  4. attacker plpgsql executes COPY TO PROGRAM (OS command) and ALTER ROLE ... SUPERUSER
How the agent worked 714 events · 355 tool calls · 1h 13m
1h 13mDuration
355Tool calls
146Reasoning steps
714Events
8Dead-ends
Agent activity over 1h 13m
Policy
1
Support
23
Repro
226
Judge
40
Variant
419
Verify
1
0:0072:42

Root Cause and Exploit Chain for CVE-2026-19633

Versions: ≤ 3.1.3 (all 3.x releases; the release-notes classify

PostgreSQL Anonymizer (postgresql_anonymizer, extension name anon) up to version 3.1.3 fails to fully validate the expressions contained in masking rules at the moment the rules are declared. The rule validator (src/input.rs::check_function driving src/walker.rs::is_untrusted_walker) only inspects T_FuncCall parse-tree nodes and resolves them against the "trusted schema / trusted function" policy. Three classes of attacker-crafted constructs reach masking-time evaluation without ever being inspected: a custom operator (an A_Expr node whose operator resolves to an attacker-defined function), a domain cast (a TypeCast through an attacker-defined DOMAIN whose CHECK constraint calls attacker code), and a view subquery / rangevar (a sub-SELECT over an attacker-owned view whose body calls attacker code). Because masking (static masking via anon.anonymize_table(), parallel static masking, replica masking, etc.) is normally executed by the extension superuser, these untrusted expressions are evaluated in the superuser security context, giving the unprivileged masked user arbitrary code execution and full superuser escalation.

  • Package/component: postgresql_anonymizer (GitLab project dalibo/postgresql_anonymizer), the anon PostgreSQL extension. Root cause lives in the Rust/pgrx core: src/input.rs, src/walker.rs (is_untrusted_walker), and the static-masking engine (src/static_masking.rs).
  • Affected versions: ≤ 3.1.3 (all 3.x releases; the release-notes classify the risk as very high on PostgreSQL 14 and instances upgraded from PostgreSQL 14 or earlier, where CREATE on schema public is granted by default).
  • Risk level: High, CVSS 8.8. An unprivileged masked database user (any role owning a schema/table, i.e. anyone allowed to declare masking rules) obtains arbitrary code execution with the privileges of the extension superuser — both SQL superuser (e.g. ALTER ROLE ... WITH SUPERUSER) and OS-level command execution as the postgres server user (e.g. COPY ... TO PROGRAM).

Impact Parity

  • Disclosed/claimed maximum impact: arbitrary code execution as the extension superuser (CVSS 8.8, "unprivileged masked users can execute arbitrary code ... malicious code can run with elevated privileges").
  • Reproduced impact from this run (full end-to-end, production path, non-sanitized): all three attack primitives (custom operator, domain cast, view subquery) hidden inside accepted masking rules were evaluated in the superuser security context when the superuser applied the masking policy. Each payload executed COPY ... TO PROGRAM (a superuser-only OS command execution capability) and wrote marker files as the postgres OS user, and the operator payload additionally executed ALTER ROLE attacker WITH SUPERUSER, turning the unprivileged attacker into a superuser. The fixed build fails closed with the anon.nosuperuser barrier error and produces no markers and no escalation.
  • Parity: full (claimed code-execution impact reproduced exactly, via the real PostgreSQL SQL endpoint of a real postgres:17 server running the real extension).

Root Cause

SECURITY LABEL FOR anon ON COLUMN ... IS 'MASKED WITH FUNCTION <expr>' is validated when the label is created (src/input.rs::check_function):

  1. the top-level node must be a T_FuncCall;
  2. when anon.restrict_to_trusted_schemas is on (default), the expression tree is walked with src/walker.rs::is_untrusted_walker, which — for each T_FuncCall node only — resolves the (schema-qualified) function name and rejects it unless the function is labelled TRUSTED or lives in a trusted schema.

is_untrusted_walker never resolves operators (A_Expr nodes), never follows casts to user-defined types/domains (whose CHECK constraints execute user-defined functions), and never expands rangevars (views) referenced by subqueries. Consequently a rule such as

MASKED WITH FUNCTION pg_catalog.int4(i OPERATOR(attack.*) 2)

passes validation although attack.* is an operator implemented by the attacker's untrusted plpgsql function. At masking time the rule text is spliced verbatim into the masking statement (static masking: UPDATE <table> SET i = pg_catalog.int4(i OPERATOR(attack.*) 2) executed via SPI by whoever runs anon.anonymize_table(), in practice the extension superuser), so the untrusted function runs with full superuser privileges.

The maintainers concluded that this static, parse-time validation approach is structurally unsustainable ("some information is not available until runtime") and replaced the attempt with a privilege barrier: [fix commit 6f102520a86bd9a9075e751d7674afb93ba7db10] "CVE-2026-19633: Escalation via custom types, operators and rangevars", released in 3.2.0 (advisory wording "3.1.4 and later"). The fix adds the anon.nosuperuser GUC (default on) which makes static, parallel static, dynamic and replica masking error out when run by a superuser (src/error.rs::should_not_be_superuser), delegating the protection to PostgreSQL's own ACLs / least-privilege model instead of trying to validate untrusted expressions.

Reproduction Steps

  1. Script: bundle/repro/reproduction_steps.sh (self-contained; run with bash bundle/repro/reproduction_steps.sh, optionally with PRUVA_ROOT=<bundle path>).
  2. What the script does:
    • resolves the source (prepared project cache mirror or GitLab) and anchors both commits: vulnerable d6989f5358131159f00f60860d744df2c685918c (= parent of the fix; verify: vulnerable tree lacks the fix hunk, fixed tree contains it);
    • builds one Docker image from postgres:17 that compiles the extension twice with the upstream build (make extension install PGVER=pg17, Rust + pgrx 0.19.1): /opt/anon-vuln and /opt/anon-fixed;
    • for each variant, twice (fresh containers a1/a2), starts a real PostgreSQL 17 server (shared_preload_libraries=anon), waits for the TCP healthcheck (pg_isready -h 127.0.0.1), then drives the whole attack through the real SQL endpoint (psql -h 127.0.0.1):
      1. superuser: CREATE EXTENSION anon CASCADE + standard SECURITY LABEL ... pg_catalog.int4/upper ... IS 'TRUSTED';
      2. unprivileged attacker role: creates schema/table, the three payload functions, a custom operator, a domain with a malicious CHECK, and a malicious view; declares the three crafted masking rules (they are accepted — this is the bypass);
      3. superuser: SELECT anon.anonymize_table('attack_<tag>.t') — in the vulnerable build the payloads run with superuser privileges; in the fixed build this fails closed with the anon.nosuperuser error;
      4. evidence: pg_roles.rolsuper of the attacker, marker files inside the container (/tmp/pruva_marker_{operator,domain,view}_<tag>), loader evidence (installed anon.so md5 matches the variant build, SHOW shared_preload_libraries = anon);
    • writes bundle/repro/runtime_manifest.json and bundle/repro/validation_verdict.json and exits 0 only if both vulnerable attempts escalate (markers + superuser) and both fixed attempts fail closed (error message, no markers, no escalation).
  3. Expected evidence of reproduction:
    • vulnerable trigger returns t from anon.anonymize_table(); the three raw single-line marker files CVE-2026-19633-RCE-{operator,domain,view}-<tag> exist (extracted with docker cp, exact bytes) plus uid=999(postgres) files proving the OS user of the server executed them; pg_roles.rolsuper = 1 for the attacker role;
    • fixed trigger raises ERROR: Anon: Static and dynamic masking cannot be used with a superuser if anon.nosuperuser is set to true. with all three markers absent and rolsuper = 0.

Evidence

  • Per-attempt transcripts (immutable, listed in runtime_manifest.json): bundle/repro/proof/{vuln,fixed}_{a1,a2}_{session,markers,loader}.txt
  • Diagnostic logs: bundle/logs/reproduction_steps.log, bundle/logs/docker_build.log
  • Key excerpts (vulnerable attempt):
    • proof/vuln_a1_marker_operator.txt (exact marker bytes): CVE-2026-19633-RCE-operator-a1, and the same for _domain_ / _view_; proof/vuln_a1_uid_operator.txt contains uid=999(postgres), proving the payload ran as the PostgreSQL server OS user;
    • proof/fixed_a1_negative_control_observation.json: strict negative-control observation (target path reached, marker absent)
    • proof/vuln_a1_evidence.txt: attacker_a1 | 1 (unprivileged attacker became SUPERUSER)
    • proof/fixed_a1_trigger.txt: ERROR: Anon: Static and dynamic masking cannot be used with a superuser if anon.nosuperuser is set to true.
  • Environment: Ubuntu 26.04 host, Docker, postgres:17 (PostgreSQL 17.11 Debian), postgresql_anonymizer built from commit d6989f5358131159f00f60860d744df2c685918c (vulnerable) and 6f102520a86bd9a9075e751d7674afb93ba7db10 (fixed), Rust stable + cargo-pgrx 0.19.1, no sanitizers.

Recommendations / Next Steps

  • Upgrade postgresql_anonymizer to 3.2.0+ (advisory: "3.1.4 and later"; the actual fix commit shipped in 3.2.0). Keep anon.nosuperuser = on (default) so no superuser can execute any masking strategy.
  • Run masking jobs (static/parallel/replica) with a dedicated least-privilege role, never with a superuser; the extension now enforces this.
  • Where pg_catalog functions must be used in rules, prefer explicit TRUSTED labels over loosening anon.restrict_to_trusted_schemas.
  • Testing recommendation: adopt the upstream regression tests elevation_via_custom_operator, elevation_via_type, elevation_via_rangevar (added with the fix) into CI for all supported PostgreSQL majors.

Additional Notes

  • Idempotency: the script is re-runnable: Docker layer caching makes the second run fast, containers/attempt work dirs are removed before each attempt, marker names are per-attempt, and both runtime_manifest.json and validation_verdict.json are rewritten deterministically. Verified by running the script twice consecutively (both runs exit 0 with identical verdicts).
  • Attack preconditions (all satisfied in the PoC, matching the advisory): the attacker is an unprivileged LOGIN role with CREATE on some schema (table owner ⇒ may declare masking rules); a superuser later executes the masking policy. The TRUSTED labels on pg_catalog.int4/pg_catalog.upper are a benign superuser-side configuration step (documented feature), not part of the attack.
  • The advisory's reproduction plan mentions dynamic masking; the superuser-context evaluation path demonstrated here (and in the vendor's own regression tests for this CVE) is the static-masking application (anon.anonymize_table()), which is exactly the code path the fix guards with anon.nosuperuser. The rule-validation bypass itself is identical for every masking strategy that consumes the same rule store.
  • Companion CVEs fixed in the same release cycle but not part of this ticket: CVE-2026-83534 (parallel masking elevation) and CVE-2026-19634 (import function SQL injection) — both are already fixed in the vulnerable anchor commit used here (6f10252^), which isolates CVE-2026-19633.

CVE-2026-19633 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:003:09
0:00
session startedaccounts/fireworks/models/glm-5p3 · CVE-2026-19633 · REPRO-20
0:04
0:06
web search
0:07
web search
0:12
0:13
0:21
0:28
0:39
0:48
1:04
1:05
1:11
1:20
2:36
2:36
extract_facts
no facts extracted
2:38
2:38
supportclaim_contract
2:47
2:48
2:48
2:50
2:50
2:50
2:55
2:57
web search
2:57
$ls -la /pruva/project-cache/ 2>&1; ls /pruva/project-cache/repo-mirrors 2>&1; cat /pruva/project-cache/.pruva/cache_manifest.json 2>&1; docker ps 2>&1 | head -5; which psql pg_ctl docker 2>&1
0.5s✗
{"cwd":"/workspace","duration_ms":95,"exit_code":1,"stderr":"","stdout":"total 4\ndrwx------ 4 vscode vscode   80 Sep  8 08:25 .\ndrwxr-xr-x 3 root   root   4096 Sep  8 08:25 ..\ndrwx------ 2 vscode vscode   60 Sep  8 08:25 .pruva\ndrwx------ 2 vscode vscode   40 Sep  8 08:25 repo-mirrors\n{\n  \"schema_version\": 1,\n  \"entries\": [\n    {\n      \"path\": \"repo-mirrors\",\n      \"reuse_class\": \"repo\",\n      \"provenance\": \"run_generated\"\n    }\n  ]\n}CONTAINER ID   IMAGE     COMMAND...
2:57
✗ error

Unknown error

3:09

Artifacts and Evidence for CVE-2026-19633

Scripts, logs, diffs, and output captured during the reproduction.

bundle/repro/Dockerfile.anon3.0 KB
bundle/repro/anon-select.sh0.7 KB
bundle/repro/proof/fixed_a1_loader.txt0.3 KB
bundle/repro/proof/fixed_a1_marker_domain.txt0.0 KB
bundle/repro/proof/fixed_a1_marker_operator.txt0.0 KB
bundle/repro/proof/fixed_a1_marker_view.txt0.0 KB
bundle/repro/proof/fixed_a1_markers.txt0.1 KB
bundle/repro/proof/fixed_a1_session.txt3.8 KB
bundle/repro/proof/fixed_a2_loader.txt0.3 KB
bundle/repro/proof/fixed_a2_marker_domain.txt0.0 KB
bundle/repro/proof/fixed_a2_marker_operator.txt0.0 KB
bundle/repro/proof/fixed_a2_marker_view.txt0.0 KB
bundle/repro/proof/fixed_a2_markers.txt0.1 KB
bundle/repro/proof/fixed_a2_session.txt3.8 KB
bundle/repro/proof/fixed_c1_loader.txt0.3 KB
bundle/repro/proof/fixed_c1_marker_domain.txt0.0 KB
bundle/repro/proof/fixed_c1_marker_operator.txt0.0 KB
bundle/repro/proof/fixed_c1_marker_view.txt0.0 KB
bundle/repro/proof/fixed_c1_markers.txt0.1 KB
bundle/repro/proof/fixed_c1_session.txt3.8 KB
bundle/repro/proof/vuln_a1_loader.txt0.3 KB
bundle/repro/proof/vuln_a1_marker_domain.txt0.0 KB
bundle/repro/proof/vuln_a1_marker_view.txt0.0 KB
bundle/repro/proof/vuln_a1_markers.txt0.2 KB
bundle/repro/proof/vuln_a1_uid_domain.txt0.1 KB
bundle/repro/proof/vuln_a1_uid_view.txt0.1 KB
bundle/repro/proof/vuln_a2_loader.txt0.3 KB
bundle/repro/proof/vuln_a2_marker_domain.txt0.0 KB
bundle/repro/proof/vuln_a2_marker_view.txt0.0 KB
bundle/repro/proof/vuln_a2_markers.txt0.2 KB
bundle/repro/proof/vuln_a2_uid_domain.txt0.1 KB
bundle/repro/proof/vuln_a2_uid_operator.txt0.1 KB
bundle/repro/proof/vuln_a2_uid_view.txt0.1 KB
bundle/repro/rca_report.md11.3 KB
bundle/repro/reproduction_steps.sh17.9 KB
bundle/repro/runtime_manifest.json7.0 KB
bundle/repro/sql/poc_evidence.sql0.4 KB
bundle/repro/sql/poc_setup.sql4.3 KB
bundle/repro/sql/poc_trigger.sql0.4 KB
bundle/repro/validation_verdict.json1.6 KB
08 · How to Fix

How to Fix CVE-2026-19633

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-19633 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-19633

Is CVE-2026-19633 exploitable?

Yes. Pruva independently reproduced CVE-2026-19633 and verified the exploit fires end-to-end in a sandboxed environment. A runnable proof-of-concept script and the full agent transcript are on this page (reproduction REPRO-2026-00362).

How severe is CVE-2026-19633?

CVE-2026-19633 is rated high severity.

What type of vulnerability is CVE-2026-19633?

CVE-2026-19633 is classified as CWE-94 (Improper Control of Generation of Code ('Code Injection')), a Privilege Escalation vulnerability.

How can I reproduce CVE-2026-19633?

Pruva provides a verified reproduction script on this page. Download it and run it inside an isolated environment such as a container or virtual machine — never against production. The reproduction was confirmed end-to-end by Pruva's automated agents.

Is the CVE-2026-19633 reproduction verified?

Yes. Pruva reproduced CVE-2026-19633 with high confidence in a sandboxed environment, capturing the full agent transcript and artifacts as evidence.
11 · References

References for CVE-2026-19633

Authoritative sources for CVE-2026-19633 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.