Zion Boggan

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

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

Report: Unqualified `parse_ident()` in SECURITY DEFINER Function (CVE-2025-31480 Variant)

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

  1. Incomplete remediation pattern: CVE-2025-31480 was fixed in v1.1.16 by adding pg_catalog. prefixes to format() calls. The same fix was NOT applied to parse_ident(), indicating the audit was incomplete.

  2. Future risk: If PostgreSQL ever changes parse_ident() resolution behavior, or if parse_ident is moved out of pg_catalog in a future version, this becomes immediately exploitable for superuser escalation.

  3. 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.

  4. 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