K

Explore

Agent Templates

Build

/Incident Report

Incident Review: Database Defacement Attack

A detailed review of the database defacement attack on February 26, 2026 - what happened, why it happened, and how we fixed it.

Serafim Korablev
Serafim Korablev21st Founder
@serafimcloud
21st

What Happened

An attacker used an automated Python script (python-httpx/0.28.1) from IP 103.136.147.84 to exploit our Supabase database through the publicly available anon API key.

The attacker first used the GraphQL endpoint (/graphql/v1) to run an introspection query, which returned the complete database schema - every table name, column, type, and relationship. With this map, they executed mass GraphQL mutations to deface records across our database. Simultaneously, they attempted REST API attacks (PATCH/DELETE) against views and tables - most REST attempts failed against views, but the GraphQL mutations succeeded due to permissive RLS policies.

67 GraphQL POST requests returned HTTP 200 within a single 3-second window, using ~15 parallel worker threads. The volume of requests caused database overload, leading to cascading statement timeouts on all legitimate traffic starting from ~4:00 AM PST.

Why It Happened

Overly permissive RLS policies on three critical tables, combined with an enabled-but-unused GraphQL endpoint:

TablePolicyVulnerability
components"Allow updating likes_count"UPDATE with qual: true, with_check: true - any anonymous user could update any column on any component
demos"Enable insert for all users" / "Enable update for all users"INSERT with with_check: true, UPDATE with qual: true - fully open writes for anyone, including anon
submissions"Allow authenticated users to submit" / "Enable update for all users"INSERT with with_check: true, UPDATE with qual: true - fully open writes for anyone

GraphQL introspection gave the attacker a complete schema map for free. Without it, they were reduced to guessing - we saw repeated demo_bookmarks.nodeId does not exist failures in their REST probes.

Losses

Data defacement: The attacker overwrote names, descriptions, and website URLs in components and demos tables with racial slurs and pornographic URLs. This was user-facing - visitors saw defaced content until the backup was restored.

The code-related fields (code, demo_code, compiled_css, tailwind_config_extension, global_css_extension) store URLs pointing to R2/CDN storage. The attacker overwrote these URLs with pornographic links. As a result, users who copied component code via the "copy prompt" feature received the HTML content of pornographic websites instead of actual component code, since the frontend fetches and serves whatever content the stored URL points to.

The actual CDN/R2 bundle files were not affected - only the database pointers to them were overwritten, and were restored via PITR backup.

No API keys, secrets, or user credentials were leaked. The attack was limited to defacement of public-facing content in the Postgres data layer.

How We Fixed It

Immediate response

  • Restored database from PITR backup (snapshot from ~2:40 AM PST, Feb 26)
  • Deleted and re-created read replicas after restore

Disabled GraphQL

  • Disabled pg_graphql extension entirely (not used anywhere in the application)
  • Eliminates schema introspection as a reconnaissance vector and closes the mutation path

RLS policies patched

components

PolicyBeforeAfter
INSERT{public}, owner-or-admin check{authenticated} only, owner-or-admin check
UPDATE ("Allow updating likes_count")qual: true, with_check: true - anyone could update any rowRemoved entirely
UPDATEOwner-or-admin checkOwner-or-admin check (unchanged, now the only UPDATE policy)

demos

PolicyBeforeAfter
INSERT{public}, with_check: true - anyone could insert any demo{authenticated} only, owner-or-admin check
UPDATEqual: true - anyone could update any demoOwner-or-admin check

submissions

PolicyBeforeAfter
INSERT{public}, with_check: true - anyone could insert{authenticated} only, must own the referenced component or be admin
UPDATE{public}, qual: true - anyone could update{authenticated} only, admin-only

Post-fix verification

  • Confirmed attacker IP 103.136.147.84 is absent from all subsequent logs
  • No PATCH/DELETE/GraphQL requests observed in the three hours after fix
  • All traffic patterns returned to normal legitimate usage
Incident Review: Database Defacement Attack | 21st | 21st