-- Migration 023 — Security hardening (Chrome ops session follow-up) -- Author: security follow-up items 1, 3, 5. Apply in the Supabase SQL editor. -- Idempotent + guarded so a re-run is safe. After applying, re-run the Security -- Advisor to confirm lint 0010 (security_definer_view) and 0011 -- (function_search_path_mutable) are cleared. begin; -- ── Item 1 (CRITICAL, advisor lint 0010) ──────────────────────────────────── -- public.founder_pricing_seats is a SECURITY DEFINER view — it runs with the -- creator's privileges and ignores RLS, exposed via the public API. Recreate -- it as security_invoker so it runs with the CALLER's privileges + respects RLS. -- (The founder counter no longer depends on this view — it now counts real -- active Stripe subscriptions directly — so this is purely closing the surface.) do $$ begin if exists (select 1 from pg_views where schemaname = 'public' and viewname = 'founder_pricing_seats') then execute 'alter view public.founder_pricing_seats set (security_invoker = on)'; end if; end $$; -- ── Item 3 — waitlist write hole ──────────────────────────────────────────── -- public.waitlist had always-true (USING(true)/WITH CHECK(true)) write policies: -- the anon API could insert/update/delete rows. Drop ALL existing policies and -- allow anon to INSERT only; updates/deletes/reads go through the service role -- (which bypasses RLS). Signups are additionally rate-limited at the API layer. alter table public.waitlist enable row level security; do $$ declare pol record; begin for pol in select policyname from pg_policies where schemaname = 'public' and tablename = 'waitlist' loop execute format('drop policy %I on public.waitlist', pol.policyname); end loop; end $$; create policy waitlist_anon_insert on public.waitlist for insert to anon with check (true); -- Remove any lingering table-level write grants from anon; keep INSERT only. revoke update, delete, truncate on public.waitlist from anon; revoke select on public.waitlist from anon; -- a signup list is not public grant insert on public.waitlist to anon; -- ── Item 5 — function search_path hardening (advisor lint 0011) ────────────── -- Flagged functions have a mutable search_path (hijackable). Pin an explicit, -- safe search_path (pg_catalog, public) — resolves the advisor without the -- breakage risk of '' on functions that reference public objects unqualified. -- Handles any overload signature. do $$ declare fn record; begin for fn in select p.oid::regprocedure as sig from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.proname in ('touch_updated_at', 'update_updated_at', 'reset_scan_count') loop execute format('alter function %s set search_path = pg_catalog, public', fn.sig); end loop; end $$; commit; -- NOTE: item 5 lists "and the other flagged functions". Run the Security Advisor -- (Supabase -> Advisors -> Security, lint 0011) for the full list; add each to -- the proname IN (...) set above and re-apply. All are the same low-risk change.