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: P4 (Suggested)
- Bug URL: https://github.com/aiven/aiven-extras/blob/main/sql/aiven_extras.sql
Title
Incomplete Remediation of CVE-2025-31480: Unqualified parse_ident() Call in aiven_extras.pg_create_publication() SECURITY DEFINER Function
Summary
The aiven_extras.pg_create_publication() SECURITY DEFINER function (owner: postgres, runs with superuser privileges) calls parse_ident() without the pg_catalog. schema prefix. This is the same vulnerability class as CVE-2025-31480 (unqualified function calls in SECURITY DEFINER functions enabling search_path hijacking) and CVE-2023-32305.
While parse_ident() is a built-in function residing in pg_catalog (which is always searched first regardless of search_path), this represents incomplete hardening: every other function call in the aiven_extras SECURITY DEFINER functions is explicitly schema-qualified with pg_catalog., making this omission inconsistent with the security pattern established after CVE-2025-31480.
Root Cause
In aiven_extras.pg_create_publication(), line ~501 of aiven_extras.sql:
CREATE OR REPLACE FUNCTION aiven_extras.pg_create_publication(
arg_publication_name text, arg_publish text,
VARIADIC arg_tables text[] DEFAULT ARRAY[]::text[])
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog'
AS $function$
DECLARE
l_ident TEXT;
l_parsed_ident TEXT[];
-- ...
BEGIN
-- ...
FOREACH l_ident IN ARRAY arg_tables LOOP
l_parsed_ident = parse_ident(l_ident); -- UNQUALIFIED: should be pg_catalog.parse_ident()
-- ...
END LOOP;
-- ...
END;
$function$
For comparison, every other function call in the same function and across all other SECURITY DEFINER functions in aiven_extras 1.1.18 uses explicit schema qualification:
- pg_catalog.format(...)
- pg_catalog.array_length(...)
- pg_catalog.left(...)
- pg_catalog.current_setting(...)
- pg_catalog.current_database()
- pg_catalog.set_config(...)
parse_ident() is the only unqualified function call across all 19 SECURITY DEFINER functions in the extension.
Steps to Reproduce
Step 1: Verify the unqualified call exists
CREATE EXTENSION IF NOT EXISTS aiven_extras;
-- Dump the function source and search for unqualified calls
SELECT prosrc FROM pg_proc
WHERE proname = 'pg_create_publication'
AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'aiven_extras');
Output contains:
l_parsed_ident = parse_ident(l_ident);
Step 2: Verify all other calls ARE qualified
-- Get all SECDEF function sources, grep for function calls
SELECT proname, prosrc FROM pg_proc
WHERE prosecdef = true
AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'aiven_extras')
ORDER BY proname;
Manual audit confirms: parse_ident is the only unqualified function call.
Step 3: Demonstrate that pg_catalog resolution prevents immediate exploitation
-- Create a shadowing function in pg_temp
CREATE OR REPLACE FUNCTION pg_temp.parse_ident(text)
RETURNS text[] AS $$
BEGIN
RAISE NOTICE 'HIJACKED!';
RETURN ARRAY['public', 'test'];
END;
$$ LANGUAGE plpgsql;
-- Explicit pg_temp call works:
SELECT pg_temp.parse_ident('test');
-- NOTICE: HIJACKED!
-- Unqualified call still resolves to pg_catalog (because pg_catalog is always first):
SET search_path TO 'pg_catalog';
SELECT parse_ident('public.test');
-- Returns {public,test} from pg_catalog version (no NOTICE)
Why This Matters Despite pg_catalog Priority
-
Incomplete remediation pattern: CVE-2025-31480 was fixed in v1.1.16 by adding
pg_catalog.prefixes toformat()calls. The same fix was NOT applied toparse_ident(), indicating the audit was incomplete. -
Future risk: If PostgreSQL ever changes
parse_ident()resolution behavior, or ifparse_identis moved out ofpg_catalogin a future version, this becomes immediately exploitable for superuser escalation. -
Defense in depth: The security best practice for SECURITY DEFINER functions (documented in PostgreSQL CVE-2018-1058 guidance) is to explicitly qualify ALL function calls. Leaving even one unqualified is a hardening gap.
-
Audit confidence: Security reviewers checking if the CVE-2025-31480 fix was complete would find this omission and question whether other unqualified calls were missed in other code paths.
Impact
- Current: No direct exploitation possible (pg_catalog is always searched first for built-in functions)
- Risk classification: Incomplete security hardening / defense-in-depth gap
- Severity: P4, Hardening deficiency, same vulnerability class as two prior CVEs (CVE-2023-32305, CVE-2025-31480)
Recommended Fix
Single-line fix in aiven_extras.sql:
- l_parsed_ident = parse_ident(l_ident);
+ l_parsed_ident = pg_catalog.parse_ident(l_ident);
References
- CVE-2025-31480: https://github.com/aiven/aiven-extras/security/advisories/GHSA-33xh-jqgf-6627
- CVE-2023-32305: https://github.com/aiven/aiven-extras/security/advisories/GHSA-7r4w-fw4h-67gp
- PostgreSQL search_path security: https://wiki.postgresql.org/wiki/A_Guide_to_CVE-2018-1058
- aiven-extras version tested: 1.1.18 (PostgreSQL 17.9 on Aiven free tier)
Source · github.com/zionsworking/security-research-notebook · writeups/aiven/pg-unqualified-parse-ident-secdef.md