diff --git a/CHANGELOG.md b/CHANGELOG.md index bc3d4dd..2b9ca16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to Workout Lens are documented here. +## [1.5.9] — 2026-05-15 + +### Security +- **Excess anon DB privileges revoked (issue #237)** — `anon` role had TRUNCATE, TRIGGER, and REFERENCES on all 14 public tables. TRUNCATE bypasses RLS at the PostgreSQL level; TRIGGER and REFERENCES are unused by PostgREST. All three revoked across every table — standard SELECT/INSERT/UPDATE/DELETE grants unchanged. +- **Duplicate `{public}` RLS policies removed (issue #237)** — `sessions`, `session_exercises`, and `muscle_activations` each had a legacy `{public}` ALL policy alongside a `{authenticated}` replacement with an identical USING clause. The `{public}` copies never granted anon access in practice (every USING checks `auth.uid()`, which is null for unauthenticated requests) but added evaluation overhead and policy-list noise. Legacy policies dropped; `{authenticated}` versions remain. + ## [1.5.8] — 2026-05-15 ### Developer / Infrastructure diff --git a/CLAUDE.md b/CLAUDE.md index 601d3c4..e63e221 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -462,3 +462,9 @@ Symptom: Home → "Siste økt" showed "Ingen økter logget ennå" even though se **Pattern to watch:** Do not combine `.limit(1)` with `.maybeSingle()` in Supabase JS v2 when the table can have multiple rows. Use `.limit(1)` with an array query and take `data?.[0]` instead. `.maybeSingle()` is only safe on queries where the base set is already guaranteed to be 0 or 1 rows (e.g. `.eq("id", id)` on a primary key). +### Issue #237 — Excess anon grants + duplicate RLS policies (resolved 2026-05-15) + +**Excess grants:** Supabase's default `GRANT ALL` on new tables gives `anon` TRUNCATE, TRIGGER, and REFERENCES — none of which PostgREST exposes. TRUNCATE bypasses RLS at the PostgreSQL level, so it is a latent risk even though PostgREST doesn't route it. Revoked via `REVOKE TRUNCATE, TRIGGER, REFERENCES ON ... FROM anon` in migration `20260516_db_permissions_cleanup.sql`. When creating a new table, only grant the privileges PostgREST actually needs — see the migration hygiene template above. + +**Duplicate policies:** `{public}` role in a PostgreSQL RLS policy means the policy applies to *all* roles including `anon`. Several tables accumulated pairs of `{public}` ALL and `{authenticated}` ALL policies with identical USING clauses (the `{public}` one was the original; the `{authenticated}` one replaced it later but the old one was never cleaned up). PostgreSQL ORs multiple permissive policies — the duplicates were harmless (every USING checked `auth.uid()`, which is null for anon, so anon access was always blocked) but added noise. When replacing a policy, always `DROP POLICY IF EXISTS` the old one in the same migration. + diff --git a/supabase/migrations/20260516_db_permissions_cleanup.sql b/supabase/migrations/20260516_db_permissions_cleanup.sql new file mode 100644 index 0000000..2894c74 --- /dev/null +++ b/supabase/migrations/20260516_db_permissions_cleanup.sql @@ -0,0 +1,39 @@ +-- Issue #237: least-privilege anon grants + duplicate RLS policy removal. +-- +-- Part 1: Revoke TRUNCATE, TRIGGER, and REFERENCES from anon on all public tables. +-- TRUNCATE bypasses RLS at the PostgreSQL level; TRIGGER and REFERENCES are never +-- used by PostgREST. Standard SELECT/INSERT/UPDATE/DELETE grants are untouched. +-- +-- Part 2: Drop legacy {public} ALL policies on sessions, session_exercises, and +-- muscle_activations. Each is an exact duplicate of a {authenticated} replacement +-- added later. PostgreSQL ORs multiple permissive policies — the {public} copies +-- never actually granted anon access (every USING checks auth.uid(), which is null +-- for unauthenticated requests) but add noise and evaluation overhead. + +-- Part 1 — revoke excess anon privileges +REVOKE TRUNCATE, TRIGGER, REFERENCES + ON public.exercise_library, + public.gym_calendar, + public.muscle_activations, + public.profiles, + public.recommendation_cache, + public.roles, + public.session_exercises, + public.session_template_exercises, + public.session_templates, + public.sessions, + public.training_groups, + public.user_gyms, + public.week_plan_days, + public.week_plans + FROM anon; + +-- Part 2 — drop legacy duplicate {public} policies +-- sessions: superseded by "Users can manage their own sessions" ({authenticated} ALL) +DROP POLICY IF EXISTS "Trenere ser egne økter" ON public.sessions; + +-- session_exercises: superseded by "Users can manage exercises in their sessions" ({authenticated} ALL) +DROP POLICY IF EXISTS "Tilgang via session" ON public.session_exercises; + +-- muscle_activations: superseded by "Users can manage activations in their sessions" ({authenticated} ALL) +DROP POLICY IF EXISTS "Tilgang via session_exercise" ON public.muscle_activations;