From 8b927f26dd95cdd038764bde2927563029cced91 Mon Sep 17 00:00:00 2001 From: Kirtan-pc Date: Fri, 7 Aug 2026 18:41:39 +0530 Subject: [PATCH] fix(db): restrict anon/authenticated read on counterfeit_reports to non-PII columns The reports_public_read policy granted anon/authenticated SELECT USING(true) over all columns of counterfeit_reports. Since the anon key is embedded in client-side code, anyone could query PostgREST and exfiltrate reporters' phone numbers, street addresses, pincodes, and exact GPS coordinates, including pending/unverified reports (PII breach #4200). Drop the permissive policy, revoke table-level SELECT for client roles, and re-grant only the non-PII display columns (brand, manufacturer, district, status, timestamps, photos, etc.). service_role API flows are unaffected. Add pgtap RLS test asserting anon can read only non-PII columns and is denied reporter_phone/address/pincode/report_location. --- ...807000000_restrict_reports_public_read.sql | 62 +++++++++ .../counterfeit_reports_public_read.test.sql | 123 ++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 supabase/migrations/20260807000000_restrict_reports_public_read.sql create mode 100644 supabase/tests/rls/counterfeit_reports_public_read.test.sql diff --git a/supabase/migrations/20260807000000_restrict_reports_public_read.sql b/supabase/migrations/20260807000000_restrict_reports_public_read.sql new file mode 100644 index 000000000..87f440921 --- /dev/null +++ b/supabase/migrations/20260807000000_restrict_reports_public_read.sql @@ -0,0 +1,62 @@ +-- ============================================================================= +-- SahiDawa — Restrict Public Read on counterfeit_reports (PII breach #4200) +-- ============================================================================= +-- PROBLEM: +-- The `reports_public_read` policy granted anon/authenticated `SELECT ... +-- USING (true)` over EVERY column of counterfeit_reports. Because the anon +-- key is embedded in client-side code, anyone could query the PostgREST REST +-- API and exfiltrate citizen reporters' personal data — reporter_phone, full +-- street address, pincode, and exact GPS coordinates (report_location) — +-- including reports still in a pending/unverified state. +-- +-- FIX: +-- Drop the permissive policy, then expose ONLY a non-PII column allowlist to +-- anon/authenticated. Because table-level SELECT in PostgreSQL overrides +-- column grants, we revoke the table-level SELECT first, then re-grant the +-- individual safe columns. The RLS row policy (`USING (true)`) keeps the +-- public transparency model working for the allowed columns. +-- +-- Blocked (PII / internal-only) columns: +-- reporter_id, reporter_phone, address, pincode, report_location +-- ip_address, report_hash, risk_score, is_escalated, +-- duplicate_group_id, snoozed_until +-- +-- IMPACT: +-- Privileged server-side flows use the RLS-bypassing service_role key, so +-- the API report flows (submit, /mine, admin list) are unaffected. Only the +-- anon/authenticated surface—which previously exposed ALL columns—is now +-- scoped to the non-PII allowlist. +-- ============================================================================= + +-- 1. Undefine the "anyone can read every column" policy. +DROP POLICY IF EXISTS "reports_public_read" ON public.counterfeit_reports; + +-- 2. Remove table-level SELECT for client roles so the column-level grants in +-- step 3 become the effective access surface. INSERT (report submission) +-- and owner UPDATE are intentionally untouched. +REVOKE SELECT ON public.counterfeit_reports FROM anon, authenticated; + +-- 3. Re-add a row-level policy so public transparency reads still return rows. +CREATE POLICY "reports_public_read_non_pii" + ON public.counterfeit_reports + FOR SELECT + TO anon, authenticated + USING (true); + +-- 4. Grant SELECT on exactly the non-PII display columns. +GRANT SELECT ( + id, + medicine_id, + scanned_barcode, + reported_brand_name, + manufacturer, + description, + pharmacy_name, + city, + state, + photo_url, + photo_urls, + district, + status, + created_at +) ON public.counterfeit_reports TO anon, authenticated; \ No newline at end of file diff --git a/supabase/tests/rls/counterfeit_reports_public_read.test.sql b/supabase/tests/rls/counterfeit_reports_public_read.test.sql new file mode 100644 index 000000000..00a6c3f2b --- /dev/null +++ b/supabase/tests/rls/counterfeit_reports_public_read.test.sql @@ -0,0 +1,123 @@ +BEGIN; + +CREATE EXTENSION IF NOT EXISTS pgtap; + +SELECT plan(6); + +-- RLS must be enabled on counterfeit_reports +SELECT ok( + ( + SELECT relrowsecurity + FROM pg_class + WHERE oid = 'public.counterfeit_reports'::regclass + ), + 'RLS enabled on counterfeit_reports' +); + +-- -------------------------------------------------------------------- +-- Seed a report row (as an admin/service-role context) that carries PII. +-- -------------------------------------------------------------------- +INSERT INTO public.counterfeit_reports +( + id, + medicine_id, + scanned_barcode, + reported_brand_name, + manufacturer, + description, + pharmacy_name, + city, + state, + address, + pincode, + photo_url, + photo_urls, + report_location, + district, + status, + reporter_phone, + created_at +) +VALUES +( + 'dddddddd-0000-4000-8000-000000000004', + NULL, + 'BC-100', + 'Acme-100', + 'Acme Pharma', + 'suspicious batch', + 'City Pharmacy', + 'Jaipur', + 'Rajasthan', + '1 Main Street, Jaipur, RJ 302001', + '302001', + 'https://cloud.example/photo.jpg', + ARRAY['https://cloud.example/photo.jpg']::text[], + ST_GeomFromText('POINT(75.7873 26.9124)', 4326)::geography, + 'verified_fake', + '+911234567890' +) ON CONFLICT (id) DO NOTHING; + +-- -------------------------------------------------------------------- +-- Act as the anonymous client. +-- -------------------------------------------------------------------- +SET LOCAL ROLE anon; + +-- Anonymous client may read only the non-PII display columns. +SELECT lives_ok( + $$ + SELECT reported_brand_name, district, status, created_at + FROM public.counterfeit_reports + WHERE id = 'dddddddd-0000-4000-8000-000000000004'; + $$, + 'anon can read non-PII display columns' +); + +-- An anonymous client must NOT be able to read reporter PII columns. +SELECT throws_ok( + $$ + SELECT reporter_phone + FROM public.counterfeit_reports + WHERE id = 'dddddddd-0000-4000-8000-000000000004'; + $$, + '42501', + NULL, + 'anon cannot read reporter_phone' +); + +SELECT throws_ok( + $$ + SELECT address + FROM public.counterfeit_reports + WHERE id = 'dddddddd-0000-4000-8000-000000000004'; + $$, + '42501', + NULL, + 'anon cannot read reporter address' +); + +SELECT throws_ok( + $$ + SELECT pincode + FROM public.counterfeit_reports + WHERE id = 'dddddddd-0000-4000-8000-000000000004'; + $$, + '42501', + NULL, + 'anon cannot read reporter pincode' +); + +SELECT throws_ok( + $$ + SELECT report_location + FROM public.counterfeit_reports + WHERE id = 'dddddddd-0000-4000-8000-000000000004'; + $$, + '42501', + NULL, + 'anon cannot read report GPS coordinates' +); + +SELECT * FROM finish(); + +ROLLBACK; \ No newline at end of file