Skip to content

Lab 02: RDBMS Architecture โ€” Mapping the Attack Surface

Field Details
Course SCIA-340 โ€” Database Security
Week 2
Difficulty โญโญ Foundational
Estimated Time 60 minutes
Topic RDBMS Architecture and Security Model
Prerequisites Lab 01 complete; psql installed; Lab 01 schema (lab01) present
Deliverables Screenshots at each checkpoint + verification script output + Security Inventory Report

Overview

Understanding what a database exposes is the first step in defending it. In this lab you will perform a complete security-oriented inventory of your Neon PostgreSQL instance โ€” querying every schema, table, extension, function, and privilege grant. This is the same reconnaissance workflow an attacker or a professional security auditor executes when first encountering a database. By the end you will have a comprehensive picture of your attack surface and will have hardened one well-known PostgreSQL default misconfiguration.


Branch Requirement

All SQL must be executed on your Neon branch. Name your branch lab-02 before starting. Create the branch from the Neon dashboard before opening your psql connection.

Neon Setup โ€” How to Connect

  1. Log in to https://neon.tech and select your lab-02 branch.
  2. Copy the connection string from Dashboard โ†’ Connection Details.
  3. Connect and switch databases:
    export DATABASE_URL="postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/scia340?sslmode=require"
    psql $DATABASE_URL
    
    \c scia340
    

Part 1 โ€” Complete Database Inventory

A full database inventory enumerates every object in every schema. Attackers run exactly these queries after gaining any authenticated foothold.

Step 1.1 โ€” All Schemas

SELECT nspname AS schema_name,
       pg_get_userbyid(nspowner) AS owner
FROM pg_namespace
ORDER BY nspname;

Note every schema present โ€” including pg_catalog, information_schema, and any user-created schemas from previous labs. Each schema is a potential target namespace.

๐Ÿ“ธ Screenshot checkpoint: Capture the full schema list.


Step 1.2 โ€” All Tables and Their Security Flags

SELECT
  schemaname,
  tablename,
  tableowner,
  hasindexes,
  hasrules,
  hastriggers,
  rowsecurity
FROM pg_tables
WHERE schemaname NOT IN ('pg_catalog','information_schema')
ORDER BY schemaname, tablename;

Pay particular attention to the rowsecurity column โ€” tables with rowsecurity = false expose all rows to any role with SELECT. This will be examined again in Step 3.2.

๐Ÿ“ธ Screenshot checkpoint: Capture the full table inventory including rowsecurity values.


Step 1.3 โ€” All Installed Extensions (Attack Surface Enumeration)

SELECT extname,
       extversion,
       pg_get_userbyid(extowner) AS owner
FROM pg_extension
ORDER BY extname;

Extensions Expand Attack Surface

Every installed extension adds new functions, operators, and sometimes system-level capabilities to your database. An extension like dblink allows outbound network connections from within SQL queries. pg_cron can schedule arbitrary SQL execution. Even uuid-ossp adds external C code to the PostgreSQL process. Know every extension installed in your database.


Step 1.4 โ€” All Functions and Their Security Model

SELECT
  n.nspname AS schema,
  p.proname AS function_name,
  CASE p.prosecdef
    WHEN TRUE THEN 'SECURITY DEFINER'
    ELSE            'SECURITY INVOKER'
  END AS security,
  pg_get_userbyid(p.proowner) AS owner
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname NOT IN ('pg_catalog','information_schema')
ORDER BY schema, function_name;

SECURITY DEFINER = Privilege Escalation Risk

A function marked SECURITY DEFINER executes with the owner's privileges, not the caller's. If a superuser owns a SECURITY DEFINER function that can be called by a low-privilege user, that user effectively gains superuser power for the duration of the function call. Always audit these functions carefully.

๐Ÿ“ธ Screenshot checkpoint: Capture the function list showing the security column for each function.


Part 2 โ€” Privilege Inventory

Knowing who can do what is the core of access control auditing.

Step 2.1 โ€” Which Roles Have Which Table Privileges

SELECT
  grantee,
  table_schema,
  table_name,
  string_agg(privilege_type, ', ' ORDER BY privilege_type) AS privileges
FROM information_schema.role_table_grants
WHERE table_schema NOT IN ('pg_catalog','information_schema')
GROUP BY grantee, table_schema, table_name
ORDER BY grantee, table_schema, table_name;

This query collapses all individual grants into one row per (grantee, table), making it easy to scan for over-privileged roles.

๐Ÿ“ธ Screenshot checkpoint: Capture the privilege summary.


Step 2.2 โ€” Schema-Level Privileges

SELECT
  n.nspname AS schema,
  pg_get_userbyid(n.nspowner) AS owner,
  array_to_string(n.nspacl, ', ') AS access_control_list
FROM pg_namespace n
WHERE n.nspname NOT IN ('pg_catalog','information_schema')
ORDER BY schema;

The nspacl column (Access Control List) encodes schema-level USAGE and CREATE grants in PostgreSQL's internal ACL format. An entry like =UC means the PUBLIC pseudo-role has both USAGE and CREATE on that schema.

๐Ÿ“ธ Screenshot checkpoint: Capture the schema ACL output.


Part 3 โ€” Security Misconfigurations

Step 3.1 โ€” Find and Fix the PUBLIC Schema Risk

PostgreSQL versions prior to 15 granted CREATE on the public schema to PUBLIC (every authenticated user) by default. This allowed any user to inject objects โ€” including malicious functions โ€” into the search path of other users.

-- Check current state before hardening
SELECT has_schema_privilege('PUBLIC', 'public', 'CREATE') AS public_can_create_in_public;
-- Revoke the dangerous default (security hardening)
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
-- Verify the revocation took effect
SELECT has_schema_privilege('PUBLIC', 'public', 'CREATE') AS public_can_create_in_public;

Expected: First query returns true; after the revoke it returns false.

CVE-2018-1058 Context

This default was documented as CVE-2018-1058. Any authenticated user could create a function named lower() in the public schema, and because public appears first in the default search_path, their malicious function would be called instead of the real pg_catalog.lower(). PostgreSQL 15+ changed this default โ€” but many older databases and cloud instances may still have the vulnerability.

๐Ÿ“ธ Screenshot checkpoint: Capture both the before (true) and after (false) query results.


Step 3.2 โ€” Find Tables Without Row-Level Security

SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname NOT IN ('pg_catalog','information_schema')
  AND rowsecurity = FALSE
ORDER BY schemaname, tablename;

Tables Without RLS

Every table listed here is one where any role with SELECT privilege can read every row โ€” regardless of whether those rows belong to them. For multi-tenant databases or tables with per-user data, this is a serious data isolation failure. RLS policies (covered in a later lab) solve this problem.

๐Ÿ“ธ Screenshot checkpoint: Capture the full list of tables lacking RLS.


Cleanup / Reset

To remove all objects created in this lab and return to the Lab 01 baseline:

-- Nothing was created in this lab โ€” only read and one revoke.
-- To restore the PUBLIC CREATE grant if needed for other testing:
GRANT CREATE ON SCHEMA public TO PUBLIC;

Warning

Do not restore the PUBLIC CREATE grant unless explicitly instructed. The revoke performed in Step 3.1 is a security improvement that should remain in place.


Assessment

Verification Script

Dr. Chen will run the following script against your Neon lab-02 branch connection string. All three checks must pass.

-- VERIFY LAB 02
SELECT
  has_schema_privilege('PUBLIC', 'public', 'CREATE')    AS public_create_revoked_false,

  (SELECT COUNT(*) FROM pg_extension)                   AS extension_count,

  (SELECT COUNT(*)
   FROM pg_proc p
   JOIN pg_namespace n ON n.oid = p.pronamespace
   WHERE n.nspname NOT IN ('pg_catalog','information_schema'))
                                                        AS user_function_count;

Expected results:

public_create_revoked_false extension_count user_function_count
false โ‰ฅ 1 โ‰ฅ 0

Note

extension_count must be at least 1 (PostgreSQL always installs plpgsql as an extension). user_function_count will be 0 on a clean Lab 02 branch unless you created functions in the additional requirement โ€” either value is accepted.


Additional Requirement

Security Inventory Report (20 pts)

Write a single SQL query that produces a unified Security Inventory Report for your Neon database. The result set must have exactly these columns: category, item, risk_level.

Requirements:

  • At least one row of category = 'Extension' for each installed extension.
  • At least one row of category = 'SECURITY DEFINER Function' for each SECURITY DEFINER function found (if none exist, include a single row with item = 'None found' and risk_level = 'LOW').
  • One row per table that lacks RLS, with category = 'Table โ€” No RLS'.
  • Assign risk_level values of 'HIGH', 'MEDIUM', or 'LOW' based on your security judgment. Be prepared to defend your assignments.
  • Use UNION ALL to combine the three (or more) SELECT statements into one result set.

Starter scaffold:

-- Security Inventory Report
SELECT
  'Extension'            AS category,
  extname || ' v' || extversion AS item,
  'MEDIUM'               AS risk_level   -- adjust based on extension
FROM pg_extension

UNION ALL

SELECT
  'SECURITY DEFINER Function' AS category,
  n.nspname || '.' || p.proname AS item,
  'HIGH'                AS risk_level
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE p.prosecdef = TRUE
  AND n.nspname NOT IN ('pg_catalog','information_schema')

UNION ALL

SELECT
  'Table โ€” No RLS'      AS category,
  schemaname || '.' || tablename AS item,
  'HIGH'                AS risk_level
FROM pg_tables
WHERE schemaname NOT IN ('pg_catalog','information_schema')
  AND rowsecurity = FALSE

ORDER BY risk_level, category, item;

Customize the risk_level values and add additional UNION ALL blocks as you see fit. Submit: the final query and a screenshot of its output with at least 3 rows.


Reflection Questions

Answer each question in 3โ€“5 sentences in your lab report.

  1. SECURITY DEFINER Escalation โ€” You found that SECURITY DEFINER functions execute as the function owner, not the caller. Why is this a privilege escalation risk? Construct a concrete attack scenario: a low-privilege app_user role, a SECURITY DEFINER function owned by a superuser, and describe exactly what app_user can do that they should not be able to do.

  2. Extensions as Attack Surface โ€” The pg_extension catalog revealed every extension installed in your database. Why is an extension like dblink or pg_cron a security concern in a production database? Which security principle does keeping unnecessary extensions installed violate, and what is the correct remediation?

  3. CVE-2018-1058 and the Public Schema โ€” Before you ran REVOKE CREATE ON SCHEMA public FROM PUBLIC, any authenticated user could create objects in the public schema. Explain the specific attack this enables: how does a malicious user exploit PostgreSQL's search_path behavior to hijack legitimate function calls made by privileged users? Why was this rated as a significant vulnerability rather than a configuration choice?


Grading Rubric

Component Points
Lab steps completed with screenshots at all checkpoints 50
Verification script โ€” all three checks pass 30
Additional requirement โ€” Security Inventory Report with โ‰ฅ 3 rows, correct columns, risk assignments justified 20
Total 100