How we audited 459 surfaces and hardened Next.js 16 Server Actions & Supabase RLS in 72 hours (NextBlock CMS)
Hey everyone! 👋
Between October 17 and October 20, we went through every founder and maintainer's trial by fire on NextBlock CMS (our open-core CMS built on Next.js 16 and Supabase, published on npm as @nextblock-cms/* with a create-nextblock CLI).
What started as a single vulnerability report on Friday turned into a full 72-hour, 10-subsystem security audit and architectural hardening pass. Since so many of us here are building production apps with Next.js 16 Server Actions and Supabase, we wanted to share a transparent breakdown of what happened, what we found, and the patterns we shipped to lock it down.
1. The Report & Same-Day Fix (Fri Oct 17 – Sun Oct 19)
On Friday, Oct 17, an independent security researcher emailed us that the public profiles table on nextblock.dev was readable using the public (anon) key shipped in the browser bundle: 85 rows with display name, avatar, website, phone, and role. Thankfully, no email addresses were in that table (they live in Supabase Auth, which was never exposed).
We fixed the reported vulnerability the same day and deployed Migration 02021 to production on Sunday, Oct 19:
profilesLockdown:profilesis now readable only by the signed-in owner of a row and by staff (ADMIN,WRITER); theanonrole gets nothing. Public review/comment cards now resolve the author's name and avatar through a computed relationship that answers only for approved rows.- Closing Adjacent Holes: While writing the fix, we found and closed two related issues in the same migration:
- Any self-registered account could change its own
rolecolumn toADMINthrough the API (the column is now pinned by both the RLS policy and a database trigger). - Page, post, and product revision history was readable by any signed-in account (now restricted to staff only).
- Any self-registered account could change its own
- Private Schema Helpers: We moved our role helpers to a private schema (
SECURITY DEFINERthere,SECURITY INVOKERwrappers inpublic) so Supabase's security advisor lints stay completely clean.
2. The Full 10-Subsystem Review (Oct 19–20)
Finding two adjacent privilege-escalation bugs while fixing the first report prompted us to audit the entire stack from top to bottom:
- Method: Inventoried 10 subsystems (~459 surfaces) and evaluated 59 candidate findings.
- Isolated Local Stack Replay: We reproduced and confirmed 44 findings independently (1 critical, 13 high; 15 refuted) on a local replay of the real stack (Supabase Postgres + PostgREST + GoTrue + the app with the production migration chain). Production (
nextblock.dev) and our public sandbox (cms.nextblock.dev) were never touched during exploit testing.
3. Key Next.js 16 & Supabase Hardening Changes (Migrations 02022 & 02023, Live Oct 20)
A. Next.js Server Actions & The WRITER Boundary
Because a Next.js Server Action is reachable as a public HTTP POST endpoint, relying on a /cms URL middleware gate isn't enough.
- Every Server Action in NextBlock now verifies its caller directly inside the action itself.
WRITERaccounts can no longer write site-wide settings (custom scripts, global CSS, the first-admin flag, security policy) and can only edit or delete media they uploaded themselves.- First-admin bootstrap is now a true one-shot: resetting a flag can never mint a second admin.
B. Database-Level MFA (is_admin_mfa()) + "Remember This Device" Server Pattern
We wanted to ensure that an admin session token carrying only a password (aal1), if used directly against the API, could never read orders, customer profiles, or secret settings.
- Inside Postgres (
02022&02023): We introduced a newis_admin_mfa()policy helper requiring anaal2session for admins who have enrolled a second factor (admins without one are never locked out). It now guardsorders,order_items, other accounts'profilesrows, the admin tier of site settings, license activations, MCP access tokens, and coupons. - Inside the Next.js CMS: Because "remember this device" admins are legitimately on a first-factor session inside the CMS UI, every CMS screen that reads those protected rows was moved to a verified-role-then-service-role pattern on the server (a shared helper, memoized per request). Nothing an editor account can see or do widened; where an editor could already open a screen (orders list, payments), they still get an empty view rather than an error.
- Deliberately Left on Plain Admin Check: Content and configuration tables that editors also write or that every CMS screen reads (pages, posts, media, navigation, shipping, tax, themes, redirects, etc.)—none hold secrets or customer PII beyond what staff already see.
C. Commerce, AI Quotas, Backups & Abuse Prevention
- Unpublished Products: Draft, scheduled, and archived products no longer leak media links, category links, or stock to the public key.
- Checkout Integrity: Coupon redemption limits are enforced atomically in the database (preventing over-redemption under concurrent checkouts); guest carts require a per-cart capability token instead of being addressable by ID; order item quantities are validated at the database.
- Auth & Rate Limits: Email second factor now locks out after a few wrong guesses; AI routes enforce per-user quotas so an editor account cannot drain the operator's model key; and pending reviews/comments have a database-level flood limit.
- Backup Restore: Full-site backup restore is gated behind an admin with a satisfied second factor, and the restore was rebuilt so accounts come back with their original password hashes.
4. Verification, Rollout & Supabase's Oct 30 Change
- Test Suite: App typecheck:
0errors. Unit tests:1,762passing across 131 files. Lint: clean. - 62-Assertion DB Posture Script: A self-contained posture script emulates the API from five distinct seats (factor-enrolled admin at
aal1andaal2, admin without a factor, editor, customer)—failing before each migration on the lines it protects, and passing all 62 assertions after. Live exploit reproductions on the replay were re-run and confirmed blocked. - Supabase Oct 30 Readiness: Ahead of Supabase's Oct 30 change (new tables no longer exposed to the Data API automatically), every table our migrations create ships with explicit grants and RLS, and our migration tooling refuses any migration file that creates a table without them.
- Researcher Follow-Up: We thanked the reporter and confirmed the fix. Their later suggestions to rotate the
anonkey and purge API/query logs were declined: theanonkey is public by design and was never the control (the policies are), and logs are kept, not purged, after an exposure.
🔄 Updating Self-Hosted & Vercel Installs
All migrations (02021–02023), generated artifacts, CLI templates, and @nextblock-cms/* npm packages have been republished. Existing projects can pull the new packages and apply all pending migrations with a single command:
npm run update🔗 Links
- Production Site: https://nextblock.dev
- Public Sandbox: https://cms.nextblock.dev
- GitHub Repository: https://github.com/nextblock-cms/nextblock
If you're building with Next.js 16 Server Actions and Supabase RLS, we’d love to hear how you handle MFA session enforcement and Server Action boundaries in your own stack! 🧱