Case Study 1 — When "the App Runs as Superuser" Turned a Bug into a Catastrophe

One SQL-injection bug, two very different outcomes — decided entirely by what role the application connected as. Least privilege is the layer that turns a breach into a contained incident instead of total compromise.

Background

Two companies had nearly identical web apps with the same SQL-injection vulnerability — a single endpoint that, due to a legacy code path, concatenated a parameter into a query (the Chapter 29 mistake). Both were attacked. The difference in outcome came down to one decision each had made long before: which database role the application connected as.

  • Company A connected its app as the postgres superuser (the default after install; "it just worked, so we left it").
  • Company B connected its app as a limited role (app_rw) with SELECT/INSERT/UPDATE/DELETE on its own tables — and nothing else.

The attack, two outcomes

The attacker exploited the injection to run arbitrary SQL through the vulnerable endpoint.

Company A (superuser): because the app role was a superuser, the injected SQL could do anything the database could do. The attacker: - read every table, including other schemas and system catalogs; - dumped password hashes and PII for the entire user base; - created a new superuser role for persistent access; - and ultimately DROPped tables and read server files. Total compromise. A single injection bug became a full breach because the app's role had unlimited power.

Company B (least privilege): the same injection let the attacker run SQL — but only the SQL their limited role was allowed to run. They could read and modify the app's own tables (bad, but bounded), and that's it: - they could not read other schemas or system catalogs; - could not DROP TABLE or CREATE ROLE (no privilege); - could not escalate to superuser; - could not touch tables the role wasn't granted.

Company B still had a serious incident (the injection bug was real and needed fixing), but it was contained: limited to the app's data, no privilege escalation, no server compromise. The blast radius was a fraction of Company A's. Same bug; vastly different damage.

The fix (both companies)

  1. Fix the injection — parameterize the vulnerable query (the root cause; Chapter 29). This is necessary regardless of roles.
  2. Apply least privilege (what saved Company B, and what Company A should have had):
-- App connects as a LIMITED role, never superuser
CREATE ROLE app_rw LOGIN PASSWORD '...';
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app TO app_rw;
-- explicitly NOT granted: superuser, CREATE ROLE, DDL, access to other schemas/system catalogs

Company A also rotated all credentials, rebuilt from clean backups, and switched the app to a least-privilege role so a future bug couldn't be catastrophic.

The analysis

  1. Parameterization prevents injection; least privilege contains it. These are different layers. Company B still had the bug — but the bug couldn't do much, because the role couldn't do much. Defense in depth means assuming one layer (here, the injection defense) fails, and limiting the damage when it does.

  2. Never run the application as superuser. The default superuser can do anything — read all data, drop everything, create roles, read files. An app role should have exactly the privileges the app needs (SELECT/INSERT/UPDATE/DELETE on its tables) and nothing more. The superuser is for administration, not for the app.

  3. Least privilege bounds every breach, not just injection. A leaked credential, a vulnerable dependency, an insider mistake — all are limited by what the role can do. It's the most leveraged single security decision you make.

  4. The cost of least privilege is near zero; the benefit is enormous. Creating a limited role takes minutes. The difference it made between Company A (total compromise) and Company B (contained incident) is the entire breach. Few security measures have a better cost/benefit.

  5. Fix root causes and limit blast radius. Company B was lucky its role was limited; both companies still had to fix the injection. Do both: eliminate the vulnerability (parameterize) and ensure that if something slips through, it's contained (least privilege).

Discussion questions

  1. Same injection bug — why was Company A's outcome catastrophic and Company B's contained?
  2. List the specific things the attacker could do as superuser that the limited role prevented.
  3. Why is "parameterize the query" necessary but not sufficient on its own?
  4. What privileges should an application role have, and which must it never have?
  5. ⭐ Beyond SQL injection, name two other breach scenarios that least privilege would also contain.