From 97380f3cddf30989f251c07d05e4db6277c83745 Mon Sep 17 00:00:00 2001 From: Johan Lindengard Date: Thu, 6 Aug 2026 11:33:19 +0200 Subject: [PATCH] =?UTF-8?q?docs(security):=20update=20RLS=20audit=20after?= =?UTF-8?q?=20third=20slice=20=E2=80=94=20no=20High=20findings=20remain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves BugReport, DeviceToken, MunicipalityWatch, ProviderSubscription, Stable and StableSpot from "Kvarvarande fynd" to "Åtgärdade fynd" with their discovery evidence, migration reference, and staging/prod verification. Updates Executive Summary, "Rekommenderad nästa slice" (now Medium/Low only), and Slutstatus (49 migrations, full parity, zero rls_disabled_in_public ERRORs in production). Also adds docs/INDEX.md entry for this audit doc (was missing) and flags rls-findings.md as superseded by it. --- docs/INDEX.md | 3 +- .../supabase-rls-security-audit-2026-08-06.md | 85 ++++++++++++------- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/docs/INDEX.md b/docs/INDEX.md index 426b1479..6ae2c52f 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -66,8 +66,9 @@ sections: | Dokument | Beskrivning | |----------|-------------| | [pentest-2026-02-15.md](security/pentest-2026-02-15.md) | OWASP ZAP pentest-rapport (februari 2026) | -| [rls-findings.md](security/rls-findings.md) | Row Level Security-analys | +| [rls-findings.md](security/rls-findings.md) | Row Level Security-analys (2026-03, företrädd av augusti-auditen nedan) | | [PENTEST-REPORT-2026-02-27.md](security/PENTEST-REPORT-2026-02-27.md) | Pentest-rapport (februari 2026, utokad) | +| [supabase-rls-security-audit-2026-08-06.md](security/supabase-rls-security-audit-2026-08-06.md) | Fullständig RLS/grants-audit (augusti 2026) -- åtgärdade fynd, kvarvarande Medium/Low, ingen High kvar | ## Testning diff --git a/docs/security/supabase-rls-security-audit-2026-08-06.md b/docs/security/supabase-rls-security-audit-2026-08-06.md index 37aea72b..fe2b0ae1 100644 --- a/docs/security/supabase-rls-security-audit-2026-08-06.md +++ b/docs/security/supabase-rls-security-audit-2026-08-06.md @@ -1,6 +1,6 @@ --- title: "Supabase RLS- och behörighetsaudit 2026-08" -description: "Full read-only RLS/grants-audit av staging + production, de fynd som åtgärdats (BookingSeries + 6 server-only-tabeller) och de som kvarstår inför nästa slice." +description: "Full read-only RLS/grants-audit av staging + production. Alla High-fynd åtgärdade (BookingSeries + 12 server-only-tabeller i tre slices); endast Medium/Low kvarstår." category: security status: active last_updated: 2026-08-06 @@ -33,9 +33,9 @@ sections: - Sex ytterligare tabeller (`PasswordResetToken`, `CustomerInviteToken`, `StableInviteToken`, `MobileToken`, `AdminAuditLog`, `StripeWebhookEvent`) hade RLS helt avstängt samtidigt som Supabase defaultar till fullt CRUD-grant till `anon`/`authenticated`/`service_role` vid tabellskapande. Två av dessa läckte **verifierat riktig data live**: `AdminAuditLog` hade 52 anon-läsbara rader i production (admin-händelser inkl. IP-adresser) och 8 i staging; `StripeWebhookEvent` hade 4 anon-läsbara rader i staging (och kunde i teorin användas för att tysta riktiga Stripe-webhooks via dedup-mekanismen). - Två av token-tabellerna (`PasswordResetToken`, `CustomerInviteToken`, `StableInviteToken`) lagrar **råa, direkt användbara tokens** (inte hashade) — läsbarhet via anon-nyckeln hade varit direkt kontokapning. -**Vad som redan är åtgärdat:** Samtliga sju tabeller ovan har fått `ENABLE ROW LEVEL SECURITY` (plus matchande policy) applicerat i **både staging och production**, och migrationshistoriken är synkad i `main`. Se [Åtgärdade fynd](#åtgärdade-fynd). +**Vad som redan är åtgärdat:** Samtliga tretton tabeller (`BookingSeries` + tolv server-only-tabeller, i tre separata slices) har fått `ENABLE ROW LEVEL SECURITY` (plus matchande policy) applicerat i **både staging och production**, och migrationshistoriken är synkad i `main`. Se [Åtgärdade fynd](#åtgärdade-fynd). -**Nuvarande säkerhetsstatus:** Inga kända aktiva dataläckor. Sex tabeller (`BugReport`, `DeviceToken`, `MunicipalityWatch`, `ProviderSubscription`, `Stable`, `StableSpot`) har fortfarande RLS avstängt i båda miljöerna och delar exakt samma sårbarhetsklass som de redan fixade — de är nästa steg, inte en aktiv krisnivå, eftersom ingen av dem hittills är bekräftat exploaterad. Se [Kvarvarande fynd](#kvarvarande-fynd) och [Rekommenderad nästa slice](#rekommenderad-nästa-slice). +**Nuvarande säkerhetsstatus (uppdaterad efter tredje slicen):** **Inga kvarvarande High-fynd.** Security Advisor i production visar **noll** `rls_disabled_in_public`-ERROR:ar — samtliga tabeller som var öppna för `anon`/`authenticated` utan autentisering är nu skyddade. Inga kända aktiva dataläckor. Det som återstår är uteslutande Medium/Low-nivå: tio staging-only RLS-disabled-tabeller (redan säkra i production, ren drift), en miljödrift på en auth-funktion, samt två mindre härdningspunkter. Se [Kvarvarande fynd](#kvarvarande-fynd) och [Rekommenderad nästa slice](#rekommenderad-nästa-slice). --- @@ -104,22 +104,50 @@ Read-only genomgång (inga ALTER/GRANT/REVOKE utfördes under själva auditen - Security Advisor omkörd efter fix: alla sex tabeller borta från `rls_disabled_in_public` i båda miljöerna. - Applicerad separat mot staging- och production-databaserna via `prisma migrate deploy`; `_prisma_migrations`-raden bekräftad (`applied_steps_count: 1`, `rolled_back_at: null`) i båda. ---- +### Ytterligare server-only-tabeller (tredje slicen) -## Kvarvarande fynd +**Tabeller:** `BugReport`, `DeviceToken`, `MunicipalityWatch`, `ProviderSubscription`, `Stable`, `StableSpot` + +**Varför de var sårbara:** + +| Tabell | Sårbarhet | +|---|---| +| `BugReport` | RLS av + full anon-grant. Anon kunde läsa/skriva/radera buggrapporter (fritext, ev. PII i beskrivningar). | +| `DeviceToken` | RLS av. Anon kunde enumerera/kapa/radera push-notification-registreringar. | +| `MunicipalityWatch` | RLS av. Anon kunde läsa e-post/bevakningsdata (PII) och skapa spam-bevakningar. | +| `ProviderSubscription` | RLS av. Anon kunde förfalska eller läsa prenumerations-/billingstatus. | +| `Stable` | RLS av. Anon kunde skapa/ändra/radera stalldata. | +| `StableSpot` | RLS av. Samma mönster som `Stable`, samma repository. | + +Ingen av dessa hade bekräftad live-läcka (till skillnad från `AdminAuditLog`/`StripeWebhookEvent` i slice 2), men samma exploaterbarhet via den publika anon-nyckeln. -### High +**Kodåtkomst-discovery (sju explicita kontrollpunkter, båda miljöerna, samma metod som föregående slice):** +1. All dataåtkomst via Prisma/server-side — bekräftat (repositories + auth-gated API-routes: `POST /api/bug-reports` kräver session, `device-tokens` kräver `getAuthUser()`, `municipality-watches` kör `withApiHandler({auth:"customer"})`, `ProviderSubscription` har ingen egen API-route alls utom Stripe-webhook + auth-gated provider-routes). +2. Ingen browser-klient använder PostgREST — 0 träffar på `.from("Stable"|"StableSpot"|"MunicipalityWatch"|"BugReport"|"DeviceToken"|"ProviderSubscription")` i hela `src/`/`scripts/`/`e2e/`, samt 0 träffar på **alla** `.from()`-anrop och direkta `/rest/v1`-fetches i hela appen. +3. Inga Edge Functions använder tabellerna — `list_edge_functions` tomt i båda projekten. +4. Inga Realtime-subscriptions — ingen av de sex finns i `supabase_realtime`-publikationen (kontrollerat via `pg_publication_tables`), och 0 `.channel()`/Realtime-kod i `src/`. +5. Inga RPC-funktioner använder tabellerna — sökte igenom alla funktionskroppar i `public`-schemat (`pg_proc.prosrc`), noll referenser. +6. Inga triggers eller views påverkas — 0 triggers på någon av de sex (`pg_trigger`); 0 application-level views i något av projekten (de 146 `pg_class`-vyerna är samtliga Postgres/Supabase-systemkataloger: `pg_catalog`, `information_schema`, `extensions`, `vault`). +7. Inga publika API-anrop förlitar sig på rå Data API — den enda medvetet publika läsvägen (`GET /api/stables`) går via appens egen Next.js-route + Prisma, aldrig via PostgREST. -Samma sårbarhetsklass och samma "server-only, noll funktionsrisk"-bevisning som de redan fixade sju — men ännu inte åtgärdade. Ingen bekräftad live-läcka på dessa (till skillnad från `AdminAuditLog`/`StripeWebhookEvent`), men samma exploaterbarhet via publik anon-nyckel. +Dessutom bekräftat innan migrationen skrevs: ingen av de sex hade någon befintlig policy eller RLS redan aktiverat i något av miljöerna (`relrowsecurity = false`, `pg_policies` tomt) — noll risk för dubblettpolicies. + +**Åtgärd:** Samma mönster — `ENABLE ROW LEVEL SECURITY` + `CREATE POLICY "Deny all via API" ... FOR ALL TO public USING (false);` för alla sex. En migration (`20260806083347_enable_rls_deny_all_remaining_server_only_tables`). + +**Verifiering (båda miljöerna):** +- `SET ROLE anon`/`SET ROLE authenticated` → 0 rader på samtliga sex. +- `SET ROLE anon; INSERT ...` mot `Stable` → `42501`-avvisning. +- 21 testfiler / 228 tester gröna under Node 20 (bug-report, device-token, municipality-watch, provider-subscription, stable/stable-spot — unit + integration). +- `npm run check:all` under Node 20 — 4/4 gröna (4653 tester). +- CI grönt på PR #471 (staging) och PR #472 (main), inklusive `Migration From Scratch` och (för main-PR:erna) `E2E Tests`/`Offline E2E Smoke`. +- Security Advisor omkörd efter fix i **båda** miljöer: alla sex tabeller borta från `rls_disabled_in_public`. I production gick `rls_disabled_in_public`-ERROR-antalet till **noll**. I staging kvarstår exakt de tio redan kända staging-only-drift-tabellerna (se Kvarvarande fynd) — inget nytt tillkommit. +- Applicerad separat mot staging- och production-databaserna via `prisma migrate deploy`; `_prisma_migrations`-raden bekräftad (`applied_steps_count: 1`, `rolled_back_at: null`) i båda. + +--- + +## Kvarvarande fynd -| Tabell | Risk | Server-only bekräftat? | -|---|---|---| -| `BugReport` | Anon kan läsa/skriva/radera buggrapporter (fritext, ev. PII) | Ja — `POST /api/bug-reports` kräver session, admin-routes admin-only | -| `DeviceToken` | Anon kan enumerera/kapa/radera push-tokens | Ja — kräver `getAuthUser()` | -| `MunicipalityWatch` | Anon kan läsa e-post/bevakningsdata, skapa spam | Ja — `withApiHandler({auth:"customer"})` | -| `ProviderSubscription` | Anon kan förfalska/läsa billing-/prenumerationsstatus | Ja — ingen egen API-route, endast Stripe-webhook (server-till-server) + auth-gated provider-routes | -| `Stable` | Anon kan skapa/ändra/radera stalldata | Ja — publik läsning (`GET /api/stables`) går via appens egen route/Prisma, inte via rå Supabase-klient | -| `StableSpot` | Samma mönster som Stable | Ja — samma repository | +**Inga High-fynd kvarstår.** Samtliga tabeller som var öppna för `anon`/`authenticated` utan autentisering via Data API är nu åtgärdade i både staging och production. ### Medium @@ -144,19 +172,17 @@ Samma sårbarhetsklass och samma "server-only, noll funktionsrisk"-bevisning som ## Rekommenderad nästa slice -Samma mönster som de två redan genomförda slicearna (migration → PR → CI → merge → `prisma migrate deploy` mot båda databaserna → `SET ROLE anon`-probe → Advisor-omkörning). +Alla High-fynd är åtgärdade. Nästa slice är enbart Medium/Low-nivå — lägre brådska, men samma verifieringsmönster (migration → PR → CI → merge → `prisma migrate deploy` mot båda databaserna → `SET ROLE anon`-probe → Advisor-omkörning) rekommenderas ändå. -| Tabell | Risk | Motivering | Föreslagen åtgärd | Uppskattad risk att bryta funktionalitet | +| Fynd | Nivå | Motivering | Föreslagen åtgärd | Uppskattad risk att bryta funktionalitet | |---|---|---|---|---| -| `BugReport` | High | Anon full CRUD på användarinskickad fritext (möjlig PII) | `ENABLE ROW LEVEL SECURITY` + `Deny all via API` | Ingen — bekräftat server-only | -| `DeviceToken` | High | Anon kan kapa/radera push-registreringar | Samma | Ingen | -| `MunicipalityWatch` | High | Anon kan läsa e-post + skapa spam-bevakningar | Samma | Ingen | -| `ProviderSubscription` | High | Anon kan förfalska/läsa billing-status | Samma | Ingen | -| `Stable` | High | Anon kan skriva/radera stalldata | Samma — **appens publika läsning påverkas inte** eftersom den går via Prisma, inte PostgREST | Ingen | -| `StableSpot` | High | Samma mönster som Stable | Samma | Ingen | -| 10 staging-only-tabeller | Medium | Drift mot production; production visar redan att "RLS på + 0 policies" är korrekt läge | `ENABLE ROW LEVEL SECURITY` (ingen ny policy behövs — matcha production) | Ingen | +| 10 staging-only RLS-disabled-tabeller (`CustomerHorseServiceInterval`, `FeatureFlag`, `Follow`, `HorseServiceInterval`, `NotificationDelivery`, `ProviderCustomer`, `ProviderCustomerNote`, `PushSubscription`, `_RouteOrderToService`, `_prisma_migrations`) | Medium | Drift mot production; production visar redan att "RLS på + 0 policies" är korrekt läge för samtliga | `ENABLE ROW LEVEL SECURITY` (ingen ny policy behövs — matcha production) | Ingen | +| `custom_access_token_hook()` miljödrift | Medium | Production `SECURITY INVOKER`/search_path NOT SET, staging `SECURITY DEFINER`/search_path satt | Harmonisera till en gemensam, medvetet vald konfiguration | Låg — kräver granskning av auth-hook-flödet innan ändring | +| `equinet-uploads`-bucket saknar gränser i production | Medium | Defense-in-depth-gap jämfört med staging | Sätt `file_size_limit`/`allowed_mime_types` på prod-bucketen, matcha staging | Ingen | +| `handle_new_user()` EXECUTE-grant till `anon`/`authenticated`/`PUBLIC` | Low | WARN i Advisor, ej praktiskt exploaterbar (trigger-only-funktion) | `REVOKE EXECUTE FROM PUBLIC, anon, authenticated` | Ingen | +| Leaked password protection avstängt | Low | Standard Supabase Auth-härdning | Slå på i Supabase Dashboard | Ingen — ingen kodändring | -**Ur scope för denna nästa slice** (kräver separat beslut/granskning): `custom_access_token_hook`-harmonisering, `equinet-uploads`-bucket-gränser i prod, `handle_new_user`-grant-städning, leaked-password-protection-inställning — inga av dessa är server-only-CRUD-hål och passar inte i samma "trivialt säkra deny-all"-kategori. +Ingen av dessa är server-only-CRUD-hål av samma typ som de tre redan körda slicearna — de kräver antingen separat beslut (auth-hook-harmonisering) eller är rena konfigurationsändringar utanför migrations-flödet (Dashboard-inställningar, bucket-policy). --- @@ -171,7 +197,8 @@ Samma mönster som de två redan genomförda slicearna (migration → PR → CI ## Slutstatus -- **Migrationer:** 48 totalt i `prisma/migrations/`, identiskt antal applicerat i både staging- och production-databasen (`_prisma_migrations`-radantal matchar exakt). -- **Paritet:** `main`, `staging`, staging-databasen och production-databasen är i full paritet avseende de två säkerhetsmigrationerna (`20260805090430_enable_rls_booking_series`, `20260805100939_enable_rls_deny_all_token_and_audit_tables`). -- **Git:** Rent arbetsträd på `main`, inga öppna PR:er för detta arbete (PR #467, #468, #469 mergade och branchar raderade). -- **Återstående arbete:** Se [Rekommenderad nästa slice](#rekommenderad-nästa-slice) — sex High-prioriterade tabeller (`BugReport`, `DeviceToken`, `MunicipalityWatch`, `ProviderSubscription`, `Stable`, `StableSpot`) samt tio Medium-prioriterade staging-only-drift-tabeller kvarstår, plus tre lägre-prioriterade härdningspunkter (auth-hook-drift, storage-bucket-gränser, `handle_new_user`-grant). +- **Migrationer:** 49 totalt i `prisma/migrations/`, identiskt antal applicerat i både staging- och production-databasen (`_prisma_migrations`-radantal matchar exakt — verifierat efter varje slice). +- **Paritet:** `main`, `staging`, staging-databasen och production-databasen är i full paritet avseende alla tre säkerhetsmigrationerna (`20260805090430_enable_rls_booking_series`, `20260805100939_enable_rls_deny_all_token_and_audit_tables`, `20260806083347_enable_rls_deny_all_remaining_server_only_tables`). +- **Security Advisor (production):** **Noll** kvarvarande `rls_disabled_in_public`-ERROR. Enda kvarvarande poster är redan kända INFO (`rls_enabled_no_policy` — RLS på, ingen policy, redan säkert) och tre WARN (`custom_access_token_hook`-search_path, `handle_new_user`-grant, leaked password protection) — samma som innan denna slice, inget nytt. +- **Git:** Rent arbetsträd på `main`, inga öppna PR:er för detta arbete (PR #467–#472 mergade och branchar raderade). +- **Återstående arbete:** Inga High-fynd kvar. Se [Rekommenderad nästa slice](#rekommenderad-nästa-slice) — enbart Medium/Low: tio staging-only-drift-tabeller, auth-hook-miljödrift, storage-bucket-gränser i prod, samt två mindre härdningspunkter (`handle_new_user`-grant, leaked password protection).