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.
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).
CVE-2026-19633 Severity
CVE-2026-19633 is rated high severity.
High — serious impact or readily exploitable. Prioritize remediation.
How to Reproduce CVE-2026-19633
pruva-verify REPRO-2026-00362 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 Proof of Reproduction for CVE-2026-19633
- 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
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
- unprivileged role declares masking rules
- extension superuser runs anon.anonymize_table() (static masking)
- masking expressions evaluated in superuser security context
- attacker plpgsql executes COPY TO PROGRAM (OS command) and ALTER ROLE ... SUPERUSER
How the agent worked
Root Cause and Exploit Chain for CVE-2026-19633
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 projectdalibo/postgresql_anonymizer), theanonPostgreSQL 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
CREATEon schemapublicis 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 thepostgresserver 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 thepostgresOS user, and the operator payload additionally executedALTER ROLE attacker WITH SUPERUSER, turning the unprivileged attacker into a superuser. The fixed build fails closed with theanon.nosuperuserbarrier error and produces no markers and no escalation. - Parity:
full(claimed code-execution impact reproduced exactly, via the real PostgreSQL SQL endpoint of a realpostgres:17server 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):
- the top-level node must be a
T_FuncCall; - when
anon.restrict_to_trusted_schemasis on (default), the expression tree is walked withsrc/walker.rs::is_untrusted_walker, which — for eachT_FuncCallnode only — resolves the (schema-qualified) function name and rejects it unless the function is labelledTRUSTEDor 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
- Script:
bundle/repro/reproduction_steps.sh(self-contained; run withbash bundle/repro/reproduction_steps.sh, optionally withPRUVA_ROOT=<bundle path>). - 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:17that compiles the extension twice with the upstream build (make extension install PGVER=pg17, Rust + pgrx 0.19.1):/opt/anon-vulnand/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):- superuser:
CREATE EXTENSION anon CASCADE+ standardSECURITY LABEL ... pg_catalog.int4/upper ... IS 'TRUSTED'; - 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); - 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 theanon.nosuperusererror; - evidence:
pg_roles.rolsuperof the attacker, marker files inside the container (/tmp/pruva_marker_{operator,domain,view}_<tag>), loader evidence (installedanon.somd5 matches the variant build,SHOW shared_preload_libraries=anon);
- superuser:
- writes
bundle/repro/runtime_manifest.jsonandbundle/repro/validation_verdict.jsonand exits 0 only if both vulnerable attempts escalate (markers + superuser) and both fixed attempts fail closed (error message, no markers, no escalation).
- resolves the source (prepared project cache mirror or GitLab) and
anchors both commits: vulnerable
- Expected evidence of reproduction:
- vulnerable trigger returns
tfromanon.anonymize_table(); the three raw single-line marker filesCVE-2026-19633-RCE-{operator,domain,view}-<tag>exist (extracted withdocker cp, exact bytes) plusuid=999(postgres)files proving the OS user of the server executed them;pg_roles.rolsuper = 1for 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 androlsuper = 0.
- vulnerable trigger returns
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.txtcontainsuid=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_anonymizerbuilt from commitd6989f5358131159f00f60860d744df2c685918c(vulnerable) and6f102520a86bd9a9075e751d7674afb93ba7db10(fixed), Rust stable + cargo-pgrx 0.19.1, no sanitizers.
Recommendations / Next Steps
- Upgrade
postgresql_anonymizerto 3.2.0+ (advisory: "3.1.4 and later"; the actual fix commit shipped in 3.2.0). Keepanon.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_catalogfunctions must be used in rules, prefer explicitTRUSTEDlabels over looseninganon.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.jsonandvalidation_verdict.jsonare 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
LOGINrole withCREATEon some schema (table owner ⇒ may declare masking rules); a superuser later executes the masking policy. TheTRUSTEDlabels onpg_catalog.int4/pg_catalog.upperare 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 withanon.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.
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{"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...Unknown error
Artifacts and Evidence for CVE-2026-19633
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-19633
FAQ: CVE-2026-19633
Is CVE-2026-19633 exploitable?
How severe is CVE-2026-19633?
What type of vulnerability is CVE-2026-19633?
How can I reproduce CVE-2026-19633?
Is the CVE-2026-19633 reproduction verified?
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.