Varonis Threat Labs uncovered a remote code execution (RCE) vulnerability in Snowflake-managed PostgreSQL that turns an ordinary database privilege into control of the underlying server by chaining together legitimate features.
The security risk extends beyond a single database bug. Our research shows how trusted, managed cloud services can still contain hidden privilege chains that bypass customer-visible logging, undermine data integrity, and create attack paths beneath the layer security teams typically monitor.
The chain starts from a non-superuser role chained together with legitimate PostgreSQL behaviors to gain full OS-level command execution on the database host, surpassing Snowflake’s published mitigations without disabling them. A successful attack could allow an attacker to tamper with or destroy database files, sabotage recovery, and hide activity outside normal SQL audit trails.
We disclosed the chain to Snowflake and they quickly remediated the issue server-side. Snowflake customers do not need to take further action. Varonis Threat Labs debuted our discovery exclusively at BSides NYC in the Cloud Village in October 2026.
Varonis Threat Labs has discovered several PostgreSQL vulnerabilities, including remote code execution in AWS RDS via PL/Perl and PL/Rust and RCE in Azure Cosmos for PostgreSQL. Together, they demonstrate that privilege-escalation chains in managed PostgreSQL are a systemic pattern. Continue reading to see how we chained together Postgres features in Snowflake and a step-by-step breakdown of the chain.
What is Snowflake Postgres?
Snowflake Postgres lets customers create, manage, and use PostgreSQL instances directly from the platform. According to Snowflake, each instance “runs a Postgres database server on a dedicated virtual machine managed by Snowflake”, and you connect to it directly using any standard Postgres client.
Like any managed database, Snowflake Postgres operates under a shared responsibility model. Snowflake manages the security of the database server, the virtual machine, and the surrounding infrastructure, while the customer is responsible for what Snowflake calls the Customer Configurable Security Controls.
A PostgreSQL instance running inside Snowflake — the starting point of our research into the managed database attack surface.
A PostgreSQL instance running inside Snowflake — the starting point of our research into the managed database attack surface.
The chain
At a high level, the chain flowed as follows:
- The starting point was snowflake_admin, a non-superuser role whose only notable attribute was CREATEROLE.
- pg_write_all_data allows tampering with the pg_cron job table, whose jobs run over the local socket as any user with no password. We used it to execute SQL as postgres and escalate to superuser.
- Snowflake blocks the usual SQL-to-OS primitives; we used PostgreSQL’s internal language and large objects to rebuild file read/write
- We then combined that file write with PostgreSQL’s archive_command — a config-defined shell command that fires on every Write-Ahead Logging (WAL) archive — to reach arbitrary command execution (RCE) as postgres.
- End result: full OS-level RCE on a managed database — defeating every published mitigation without disabling any of them.
Five links take a constrained, non-superuser role all the way to an OS command execution. Each one is an individually documented, intended PostgreSQL behavior. When chained together, they are catastrophic and enable code execution within the instance.
How the chain works link by link
How the chain works link by link
How the chain works link by link
- CREATEROLE as the foothold. The journey starts from snowflake_admin, whose only notable attribute is CREATEROLE. Since PostgreSQL 14, that attribute alone is enough to self-grant predefined system roles, including pg_read_all_data and pg_write_all_data. None of these roles are a superuser, which is explicitly excluded from this set.
- Self-granting data-access roles. Using CREATEROLE, the role grants itself pg_read_all_data and pg_write_all_data, instantly gaining read and write access across every table and system catalog in the cluster.
- Hijacking pg_cron to reach superuser. With pg_write_all_data now in hand, we can write to the pg_cron job table — a regular table that the scheduler trusts implicitly. Because pg_cron runs jobs over the local socket as any user with no password, rewriting a job's username to postgres lets us execute SQL as superuser and plant a SECURITY DEFINER backdoor, a function that runs with the privileges of the user who created it rather than the user who calls it (like a setuid program in Unix).Once it's owned by postgres, any low-privileged user who calls it executes as superuser.
- Rebuilding blocked primitives via internal. As a superuser, we can create functions using LANGUAGE internal, which binds new SQL names to the compiled C symbols still present in the binary, including the very functions Snowflake blocked by name.
- Arbitrary file read and write. Using these rebound functions, lo_export writes anywhere the postgres OS user can, and the rebound pg_read_file pulls any file off disk.
- From file write to command execution. With arbitrary file write, we overwrote postgresql.auto.conf via lo_export to inject a shell command into archive_command which PostgreSQL executes through /bin/sh -c every time it archives a completed WAL segment (WAL is PostgreSQL's transaction journal, recording every change before it reaches disk). A custom C extension served as an alternative path.
CREATEROLE is a privilege-escalation primitive
CREATEROLE is widely treated as minor, but in practice it is one of PostgreSQL’s most underestimated privileges.
PostgreSQL 16 tightened CREATEROLE, but one edge survived. It can still self-grant predefined pg_* system roles, which have no “creator” and never requires a superuser to grant. snowflake_admin has rolcreaterole = true.
The CREATEROLE privilege is all we start with, and all we need.
In our reserach, we granted broad data access across the entire instance successfully.
With pg_read_all_data we could read every table and catalog, including pg_authid — which exposes each role’s credential verifier. Two superuser roles (postgres and snowflake_superuser) had rolsuper=true, but we needed a way to execute SQL as one of them.
That’s where pg_cron came in. The extension loads via shared_preload_libraries and, for each scheduled job with pg_write_all_data. We could write directly to the cron.job table, breaking the assumption that only trusted components modify it. We rewrote a job’s username to “postgres” and set its command to create a SECURITY DEFINER function that would run any SQL as superuser and grant itself back to us.
When the scheduler fired, it ran as postgres, created our function, and handed us the keys. We promoted our own role to snowflake_superuser and reached superuser without ever calling ALTER ROLE a SUPERUSER.
In this way, we reached full PostgreSQL superuser access without ever calling ALTER ROLE ... SUPERUSER which Snowflake blocks.
Turning Superuser into file, read, and write
On self-hosted PostgreSQL, a superuser is effectively code execution. A managed provider’s job is to sever that equivalence. Reaching superuser only meant we’d arrived at the boundary Snowflake defends.
Superuser gave us the foundation to rebuild file read/write primitives — not command execution itself. Reading files enables reconnaissance; writing files stages payloads; the next stage turns into a running process.
We reached for the textbook SQL-to-OS primitives. Snowflake had blocked each one:
- COPY ... TO PROGRAM — blocked
- ALTER SYSTEM — blocked
- pg_read_file() / pg_read_server_files — blocked
Every well-known path was a dead end — exactly where most security assessments stop.
Finding the door: the internal language
Instead of attacking the functions, we asked how they’re built. Re-aliasing an internal C symbol via CREATE FUNCTION ... LANGUAGE internal is documented behavior. What this research shows is that a provider’s catalog-level blocklist can be fully reconstructed — because the compiled symbols remain in the binary — and chained to OS-level RCE.
The PostgreSQL documentation is remarkably candid:
“Granting users the EXECUTE privilege on pg_read_file(), or related functions, allows them the ability to read any file on the server that the database server process can read; these functions bypass all in-database privilege checks. … a user with such access is able to read the contents of the pg_authid table … as well as read any table data in the database.”
We started by confirming the internal language even existed on the instance. \dL lists procedural languages:
postgres=# \dLinternal is present. A direct binding to the C functions compiled into the server, including the very ones Snowflake “blocked., Creating LANGUAGE internal functions is superuser-only.
It worked. my_read_file does everything pg_read_file did with zero privilege checks. Snowflake’s block was a lock on the door handle; we bolted on a second handle. Every “blocked” primitive was one CREATE FUNCTION away.
Note: still confined to our own single-tenant VM
Before attempting code execution, this one primitive was enough to gut the host:
- /etc/passwd — confirming postgres runs as uid=26
- Snowflake’s internal tenant mTLS private key and certificate
- A tenant AWS STS credentials sitting in plaintext
Reading passwd and key through our rebuilt my_read_file wrapper. The same C symbol Snowflake blocked, now exposed under a name we own.
Reading passwd and key through our rebuilt my_read_file wrapper. The same C symbol Snowflake blocked, now exposed under a name we own.
From file write to RCE
Once you understand that “blocked” means “the SQL name was removed,” the rest falls the same way.
Listing directories. We rebound pg_ls_dir() through internal (prosrc = ‘pg_ls_dir’) alongside pg_stat_file to walk directories and locate files.
Writing files. lo_export(oid, path) writes anywhere the postgres user can with no sandboxing.
With arbitrary file write in hand, we targeted postgresql.auto.conf — the configuration file PostgreSQL reads on reload. We staged a payload as a large object and exported it directly over the config file using our rebuilt lo_export wrapper.
The payload overwrote archive_command — a GUC whose value PostgreSQL runs through /bin/sh -c on every completed Write-Ahead Log (WAL) WAL segment, as the postgres OS user. It is the cleanest server-side command sink in PostgreSQL, normally gated behind ALTER SYSTEM, which we now control through a different path.
From a single non-superuser role, we had full OS-level RCE — uid=26, the postgres user, on the instance managed by Snowflake.
As an alternative path, we also wrote and compiled a custom C extension directly on the host, loaded it via LANGUAGE c, and registered a SQL-callable shell function. This provided a durable, repeatable command-execution primitive but the archive_command method was sufficient to prove the chain.
Contents of postgresql.auto.conf lo_export overwrite.The archive_command now points to our payload.
Contents of postgresql.auto.conf lo_export overwrite.The archive_command now points to our payload.
Every mitigation and how it was bypassed
|
Blocked Operation |
Bypass Used |
|
ALTER SYSTEM |
Direct file write to postgresql.auto.conf via lo_export |
|
COPY TO PROGRAM |
archive_command injection and custom .so extension |
|
pg_read_file() |
LANGUAGE internal wrapper bound to the same symbol |
|
ALTER ROLE ... SUPERUSER |
GRANT snowflake_superuser via SECURITY DEFINER backdoor |
|
COPY FROM/TO FILE |
lo_import / lo_export large-object interface |
Snowflake's hardening in action — every direct attempt to read files, execute programs, or grant dangerous roles is blocked.
Snowflake's hardening in action — every direct attempt to read files, execute programs, or grant dangerous roles is blocked.
What threats could do next
A leaked admin credential is normally a rotate-and-move-on problem. This chain turns it into a permanent foothold for threats.
The attacker creates a stored function that executes OS commands. Because it's a database object, it persists independently of the credential that created it. Credential rotation doesn't touch it. Because the function runs as the postgres OS account — owner of the entire data directory — an attacker can bypass the database entirely:
- Invisible data tampering. Editing on-disk heap files alters table contents without passing through SQL. Constraints, triggers, row-level security, and in-database audit logging are all bypassed. There is no SQL statement to record, and the change has no actor attribution from the database perspective.
- Recovery sabotage. Modifying or removing WAL segments breaks point-in-time recovery and replication to standbys. Corrupting pg_control or clog can render committed transactions unreadable or prevent the cluster from starting entirely.
- Ransomware-equivalent destruction. Data files can be truncated, deleted, or encrypted in place. In the encryption case, the database cannot read its own data, and backups may not cleanly resolve the situation if they post-date the compromise.
- Local log tampering. PostgreSQL logs can be altered or removed to obscure activity. (Logs shipped off-host to a centralized system would not be affected. This conceals on-instance evidence only.)
Defense recommendations
Snowflake remediated this chain server-side; managed customers require no action. The value below is for anyone who operates PostgreSQL, because every link in the chain is stock PostgreSQL working as designed.
The root cause is an over-granted CREATEROLE attribute combined with features that trust caller-supplied identity.
For managed PostgreSQL customers:
- Use Snowflake token-based authentication. Replace static PostgreSQL passwords with short-lived Snowflake-issued tokens. This binds authentication to Snowflake's identity layer (MFA, SSO, centralized audit) and limits the window an attacker has if a credential is compromised.
- Enable network policies. Restrict which IP addresses and ranges can connect to your Snowflake Postgres instance. Use AWS PrivateLink or Azure Private Link instead of exposing the instance over the public internet.
- Apply least-privilege roles. Do not use snowflake_admin for application workloads. Create dedicated roles scoped to specific schemas and tables. Never grant CREATEROLE to application or service-account roles — it was the entry point for the entire chain.
- Implement granular role-based access control (RBAC). Restrict database user privileges tightly. Define granular user and role-based access controls based strictly on operational needs, keeping the scope and duration of privileged access to a minimum.
- Monitor for privilege escalation. Alert on GRANT statements that assign CREATEROLE, pg_read_all_data, pg_write_all_data, or membership in system roles. Periodically review pg_cron jobs and SECURITY DEFINER functions for anything unexpected.
For teams running PostgreSQL independently:
- Treat CREATEROLE as near-superuser. Revoke it from application roles. Constrain pg_cron so cron.job.username can only be the session user, never a high-privilege role like postgres.
- Limit internal and untrusted function creation. Prevent non-platform roles from creating LANGUAGE internal or LANGUAGE c functions. Hiding a function name is not the same as removing the capability.
- Protect configuration files. Make postgresql.auto.conf immutable or block lo_export writes to the data directory; archive_command runs through /bin/sh -c on every WAL archive.
- Confine the engine. Run postgres under a real SELinux domain, disable unused extensions, and do not expose PostgreSQL ports publicly.
We followed coordinated disclosure with Snowflake:
-
May 31, 2026 — Reported. The full privilege-escalation chain and resulting RCE were reported through Snowflake's security disclosure process.
-
June 1, 2026 — Acknowledged the report and began internal triage
- June 9, 2026 — Snowflake confirmed the finding, stating: "We reviewed the behavior internally and agree that local code execution inside the Snowflake Postgres VM crosses an intended security boundary." The relevant issues were remediated prior to publication.
We thank Snowflake's security team for their swift response and collaboration throughout the disclosure process. - October 17, 2026 — We debuted our findings exclusively at BSides NYC in the Cloud Village.
The bottom line
A single non-superuser role with CREATEROLE became full OS-level RCE on a managed PostgreSQL instance, defeating every published mitigation. None of the building blocks are novel alone, the escalation used PostgreSQL exactly as designed.
Because this attack lives beneath the database, most of it plays out where a managed customer simply can't see — so your leverage is narrower than it feels. Scope the roles you create with least privilege and lean on a platform whose security team takes this kind of hardening seriously as Snowflake's did here.
Snowflake had already hardened against the obvious SQL-to-OS paths before we ever showed up — real defense in depth, not box-checking. We reported the full chain in May 2026, and they moved quickly and remediated it server-side.
A managed database shifts operational burden to the provider but does not erase the customer’s role. Customers still own role provisioning and privilege scoping. If you run PostgreSQL yourself, treat least-privilege role design as a first-class control.
This is not just a database bug, it’s a cloud trust-boundary issue. Managed databases reduce operational burden, but they don’t eliminate risk from over-privileged roles, hidden platform-layer activity, or feature interactions that bypass normal audit trails. While Snowflake has remediated the chain, the broader lesson is that “managed” does not mean “invisible risk is gone.”
Stay up to date on the threat landscape with Varonis Threat Labs.
What should I do now?
Below are three ways you can continue your journey to reduce data risk at your company:
Schedule a demo with us to see Varonis in action. We'll personalize the session to your org's data security needs and answer any questions.
See a sample of our Data Risk Assessment and learn the risks that could be lingering in your environment. Varonis' DRA is completely free and offers a clear path to automated remediation.
Follow us on LinkedIn, YouTube, and X (Twitter) for bite-sized insights on all things data security, including DSPM, threat detection, AI security, and more.