Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

39 changes: 39 additions & 0 deletions supabase/migrations/20260516_db_permissions_cleanup.sql
Original file line number Diff line number Diff line change
@@ -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;