Cookbook recipe

Public Schema Unrestricted Access

Applies to PostgreSQL 13–17 Last reviewed May 2026 Grounded in source
Estimated investigation4 min

Scenario

Scenario A security review finds that all database users can CREATE tables and functions in the public schema (pre-PostgreSQL 15 default behavior). A low-privilege analytics user exploited this to create a function named now() in the…

Investigation Path

Scenario

A security review finds that all database users can CREATE tables and functions in the public schema (pre-PostgreSQL 15 default behavior). A low-privilege analytics user exploited this to create a function named now() in the public schema. Since public is first in the default search_path, all subsequent calls to now() in that session execute the malicious function instead of pg_catalog.now() — a classic search-path injection attack.

How to Identify

Conditions:

  • pg_namespace.nspacl for public schema includes =UC/postgres (CREATE for PUBLIC)
  • Any user can run CREATE TABLE in public without special grants
  • search_path includes public before pg_catalog (allows function shadowing)
  • PostgreSQL version < 15 (PG15 removed default CREATE on public schema for PUBLIC)

Analysis Steps

-- Check public schema ACL
SELECT nspname, nspacl
FROM pg_namespace
WHERE nspname = 'public';
-- nspacl containing '=UC/postgres' = all users have USAGE + CREATE on public schema

-- Check if any user can create in public
SELECT has_schema_privilege('some_low_priv_user', 'public', 'CREATE') AS can_create;

-- Check for suspicious objects in public schema
SELECT schemaname, objectname, objecttype, owner
FROM pg_catalog.pg_depend d
JOIN pg_catalog.pg_class c ON d.objid = c.oid
WHERE schemaname = 'public'
ORDER BY owner;

-- Check for function overrides in public schema
SELECT n.nspname, p.proname, p.proowner::regrole AS owner,
       pg_get_function_identity_arguments(p.oid) AS args
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE n.nspname = 'public'
  AND p.proname IN ('now', 'current_timestamp', 'pg_sleep', 'md5', 'encode');
-- Any of these = potential function shadowing attack

Pitfalls

  • In PostgreSQL 14 and earlier, PUBLIC has CREATE privilege on the public schema by default. PostgreSQL 15 changed this — new installs no longer grant CREATE to PUBLIC on public schema.
  • Removing CREATE from public schema doesn’t remove existing objects — it only prevents new ones.
  • search_path order matters: if public comes before pg_catalog, user-created functions in public can shadow built-in functions. Always set search_path = "$user", public and never include untrusted schemas first.
  • The fix REVOKE CREATE ON SCHEMA public FROM PUBLIC only affects the schema — tables already in public are still accessible to anyone with USAGE on the schema.
  • For new databases: also REVOKE ALL ON DATABASE mydb FROM PUBLIC to prevent unauthorized connections.

Resolution Approach

Revoke CREATE on the public schema from PUBLIC. Audit for any objects created by non-owners. For new databases: apply the PostgreSQL 15 default behavior explicitly. Set search_path to "$user", public to prevent schema injection.

This is a Pro lesson

Get every Learning Pathway and cookbook recipe — grounded in PostgreSQL source code, with diagnostics, fixes, and prevention for each topic.

Continue this lesson to learn:

  • Mitigation Actions
  • All 36 Learning Pathway lessons
  • 170+ cookbook recipes
  • Source-grounded diagnostics & fixes

Secure checkout Cancel anytime Source-grounded

Career Impact

This scenario builds production judgment and operational confidence under pressure.

Open Career Dashboard →

Keep going

Related & next steps

Was this helpful?

← All cookbook recipes