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
- Log in to https://neon.tech and select your
lab-02branch. - Copy the connection string from Dashboard โ Connection Details.
- Connect and switch databases:
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)¶
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;
-- 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 eachSECURITY DEFINERfunction found (if none exist, include a single row withitem = 'None found'andrisk_level = 'LOW'). - One row per table that lacks RLS, with
category = 'Table โ No RLS'. - Assign
risk_levelvalues of'HIGH','MEDIUM', or'LOW'based on your security judgment. Be prepared to defend your assignments. - Use
UNION ALLto 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.
-
SECURITY DEFINER Escalation โ You found that
SECURITY DEFINERfunctions execute as the function owner, not the caller. Why is this a privilege escalation risk? Construct a concrete attack scenario: a low-privilegeapp_userrole, aSECURITY DEFINERfunction owned by a superuser, and describe exactly whatapp_usercan do that they should not be able to do. -
Extensions as Attack Surface โ The
pg_extensioncatalog revealed every extension installed in your database. Why is an extension likedblinkorpg_crona security concern in a production database? Which security principle does keeping unnecessary extensions installed violate, and what is the correct remediation? -
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'ssearch_pathbehavior 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 |