React2Shell + Node2Shell + Postgres RLS: A Combined Audit for AI-Built Next.js and Supabase SaaS
Three independent failure classes can sit in the same app: an RSC protocol RCE, a Node runtime memory bug, and tenant data exposed by missing Row-Level Security.
01. Observed Symptoms & Why Standard Package Audits Fail
Observed (identifiers): React2Shell is CVE-2025-55182, rated CVSS 10.0, in the React Server Components Flight protocol. Node2Shell is CVE-2026-111497 in the Node.js Brotli decoder, with fixes in v22.23.4+, v24.22.1+ and v26.12.1+ (https://nodejs.org/en/blog/vulnerability/). The React advisory is not on the primary-source list supplied for this post. Fetch it independently: https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components.
Symptoms: (1) The app was scaffolded quickly by an AI coding tool and has never had a security review. (2) The frontend works for you but you have not tested what a second tenant's user can read. (3) Stripe subscriptions update correctly in testing, but nobody has proven the webhook rejects forged calls. (4) npm audit shows a few moderate findings and nothing else, which feels reassuring and is not.
Why package audits fail here: npm audit sees the React and Next.js versions. It does not see the Node binary (Node2Shell), your database policies (RLS), or your webhook logic. Only one of the three risks lives in the lockfile.
Inference (AI-generated code): Code generators tend to optimize for 'the feature works'. Typical gaps are tables created without RLS, policies written as true for convenience, privileged keys placed in client code, and webhook handlers that parse JSON before verifying the signature. This is a pattern to test for, not a claim about any specific tool. Verify each project on its own evidence.
02. Root-Cause Forensic Analysis (Runtime & Protocol Level)
Risk 1, React2Shell (Observed, public advisory, verify): The flaw is in how React Server Components deserialize Flight protocol payloads, reachable by an unauthenticated request to affected server-function endpoints in apps using the App Router. The public advisory lists the react-server-dom-webpack, react-server-dom-turbopack and react-server-dom-parcel packages and fixes in React 19.0.1, 19.1.2 and 19.2.1, with corresponding patched Next.js releases. Confirm exact versions on the advisory, then check what you actually installed.
Risk 2, Node2Shell (Observed): out-of-bounds write in the native Brotli decoder, reachable through inbound Content-Encoding: br and outbound fetch() auto-decompression. Your Next.js server runs on Node, so the Node patch level matters as much as the Next.js version. See PAIN-2026-002 for the detailed treatment.
Risk 3, RLS (Observed, Postgres behaviour): Supabase exposes tables through PostgREST using the anon and authenticated roles. A table in the public schema with RLS disabled is readable and writable by anyone holding the public anon key. A policy of USING (true) is equivalent to no policy. The service_role key bypasses RLS entirely, so it must never reach a browser bundle. Claims in user_metadata are user-editable, so tenant identity must come from app_metadata or a server-side lookup.
The first two queries below enumerate tables with no RLS and policies that are effectively open. The shell commands check the React, Next.js and Node versions and look for a leaked privileged key in built client assets.
-- 1. Tables in public without RLS (exposed through the Data API if granted)
select n.nspname as schema, c.relname as table, c.relrowsecurity as rls_on, c.relforcerowsecurity as rls_forced
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r' and not c.relrowsecurity;
-- 2. Policies that are effectively open
select schemaname, tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public' and (qual = 'true' or with_check = 'true' or qual is null);
npm ls next react react-dom react-server-dom-webpack react-server-dom-turbopack react-server-dom-parcel
node -v # compare against v22.23.4+ / v24.22.1+ / v26.12.1+
npm run build
grep -r 'service_role' .next/static || echo 'no service_role string in client assets'
grep -rE 'sk_live_|whsec_' .next/static || echo 'no Stripe secrets in client assets'
03. Immediate Zero-Downtime Triage (Non-Breaking Mitigation)
Ordering rule for RLS (Observed, Postgres behaviour): enabling RLS on a table with no policies denies all access to non-privileged roles, which is an outage. Create the policy first, then enable RLS. Policies can exist on a table before RLS is switched on.
Tenant identity: the policy below reads tenant_id from app_metadata in the JWT. Set that claim server-side only, for example from an admin API or an auth hook. Never use user_metadata for authorization. Wrapping auth.jwt() in a select lets Postgres evaluate it once per statement instead of per row.
Stripe webhooks: verify the signature against the raw request body before anything else, and make processing idempotent on event.id. The handler below returns 400 for forged calls and 200 for replays.
React2Shell and Node2Shell triage: upgrade React and Next.js to the patched releases from the React advisory and rebuild on a fixed Node line. If you cannot deploy today, apply the br-request rejection from PAIN-2026-002 at the edge, and restrict direct internet access to the Next.js origin.
create policy invoices_tenant_isolation on public.invoices
for all to authenticated
using (tenant_id = (((select auth.jwt()) -> 'app_metadata') ->> 'tenant_id')::uuid)
with check (tenant_id = (((select auth.jwt()) -> 'app_metadata') ->> 'tenant_id')::uuid);
alter table public.invoices enable row level security;
alter table public.invoices force row level security; -- applies the policy to the table owner too
create index if not exists invoices_tenant_id_idx on public.invoices (tenant_id);
import Stripe from 'stripe';
import { createClient } from '@supabase/supabase-js';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const admin = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
export async function POST(req: Request) {
const raw = await req.text(); // raw body, never req.json() before verification
const sig = req.headers.get('stripe-signature');
if (!sig) return new Response('missing signature', { status: 400 });
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(raw, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response('invalid signature', { status: 400 });
}
const { error } = await admin.from('stripe_events').insert({ id: event.id, type: event.type });
if (error?.code === '23505') return new Response('duplicate', { status: 200 }); // already processed
if (error) return new Response('storage error', { status: 500 });
// ...handle event.type here
return new Response('ok', { status: 200 });
}
04. Permanent Remediation & Cryptographic SBOM Attestation
Permanent fix, in order of blast radius: (1) patch React and Next.js to the releases named in the React advisory; (2) move the runtime to a fixed Node line and pin the base image by digest; (3) bring every public table under RLS with tested policies, and add a CI check that fails when query 1 above returns rows; (4) keep SUPABASE_SERVICE_ROLE_KEY and STRIPE_WEBHOOK_SECRET server-only, rotate them if they ever appeared in a client bundle or a repo; (5) add RLS tests that sign in as two tenants and assert cross-tenant reads return zero rows.
Attestation: emit a CycloneDX SBOM for the production image and sign it against the image digest (PAIN-2026-005), so you can show which React, Next.js and Node versions shipped in each release.
Primary source: https://nodejs.org/en/blog/vulnerability/. Additional source outside the supplied list: the React advisory linked in Section 01.
HadayaLab operates with 45x Google Cloud & DeepMind Skill Badges (https://www.skills.google/public_profiles/49cdbdb6-20d4-4661-934a-d882c7f5c6cf). We audit AI-built Next.js + Supabase SaaS end to end (dependencies, runtime, RLS, Stripe) and hand back a prioritized fix list with evidence.
Frequently Asked Questions
What is React2Shell and does it affect my Next.js App Router app?
React2Shell is CVE-2025-55182, a CVSS 10.0 flaw in the React Server Components Flight protocol. Apps using React Server Components through the Next.js App Router are in scope. Run npm ls on next, react and the react-server-dom-* packages and compare with the fixed versions in the React advisory.
How do I audit a Next.js + Supabase app built with Lovable, Cursor, Bolt or Claude Code?
Run three checks: dependency and Node versions (React2Shell and Node2Shell), the two RLS queries in this post (tables without RLS and open policies), and a webhook review (raw-body signature verification plus idempotency). Then grep built client assets for service_role and other secrets.
Why is Row-Level Security the first thing to check in an AI-generated SaaS?
Supabase exposes public-schema tables through its Data API. A table without RLS is readable and writable with the public anon key, and a policy of true is equivalent to none. Generated code often works perfectly for a single user while exposing every tenant's data, so you need an explicit two-tenant test.
How do I secure Stripe webhooks in a Next.js route handler?
Read the raw body with req.text(), verify it with stripe.webhooks.constructEvent and your webhook secret, reject on failure, and record event.id in a table with a unique constraint so replays return 200 without reprocessing. Never call req.json() before verification.