diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e343e2..d6f81a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Every release corresponds to a `staging` to `main` pull request and a matching `vX.Y.Z` tag on `main`. +## [0.2.1] - 2026-07-01 + +### Security + +- Resolved Supabase Security Advisor warnings originating from our schema: + `set_updated_at` is now `security invoker` with an empty `search_path` (fixes + "function search path mutable"), and the public contact-form insert policy is + bounded (non-empty, sane lengths) instead of `with check (true)` (fixes "RLS + policy always true"). Added as `supabase/migrations/0002_security_advisor.sql`. + ## [0.2.0] - 2026-07-01 ### Added @@ -124,6 +134,7 @@ marketing site rebrand. - Invalid nested `` in template cards (hydration error). - Tolerate a missing `text_align` column before the migration runs. +[0.2.1]: https://github.com/BiSemaphore/binarysemaphore.com/releases/tag/v0.2.1 [0.2.0]: https://github.com/BiSemaphore/binarysemaphore.com/releases/tag/v0.2.0 [0.1.4]: https://github.com/BiSemaphore/binarysemaphore.com/releases/tag/v0.1.4 [0.1.3]: https://github.com/BiSemaphore/binarysemaphore.com/releases/tag/v0.1.3 diff --git a/package.json b/package.json index 7522f76..3c1558e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "binarysemaphore", - "version": "0.2.0", + "version": "0.2.1", "private": true, "scripts": { "dev": "next dev", diff --git a/supabase/migrations/0002_security_advisor.sql b/supabase/migrations/0002_security_advisor.sql new file mode 100644 index 0000000..900017c --- /dev/null +++ b/supabase/migrations/0002_security_advisor.sql @@ -0,0 +1,41 @@ +-- Address Supabase Security Advisor warnings that originate from our schema. +-- Safe to re-run (idempotent). + +-- 1) "Function Search Path Mutable" on public.set_updated_at. +-- Pin the function to security invoker with an empty search_path so it can't be +-- hijacked by a caller's search_path. now() is in pg_catalog (always resolved). +create or replace function public.set_updated_at() +returns trigger +language plpgsql +security invoker +set search_path = '' +as $$ +begin + new.updated_at = now(); + return new; +end; +$$; + +-- 2) "RLS Policy Always True" on public.contact_messages. +-- Replace `with check (true)` with bounded validation. Still an open, insert-only +-- public form (anon + authenticated), but the payload must be non-empty and sane. +drop policy if exists "Anyone can submit a contact message" on public.contact_messages; +create policy "Anyone can submit a contact message" + on public.contact_messages + for insert + to anon, authenticated + with check ( + char_length(name) between 1 and 200 + and char_length(email) between 3 and 320 + and char_length(message) between 1 and 5000 + ); + +-- Note: warnings for public.rls_auto_enable() (SECURITY DEFINER, world-executable) +-- are NOT from this repo — that function was created directly in the dashboard. +-- If it is unused, drop it in the SQL editor after confirming: +-- drop function if exists public.rls_auto_enable(); +-- Or, to keep it but stop anon/authenticated from executing it: +-- revoke execute on function public.rls_auto_enable() from anon, authenticated, public; +-- +-- "Leaked Password Protection Disabled" is an Auth setting (Authentication -> +-- Policies): a dashboard toggle, low risk here since sign-in is OAuth-only. diff --git a/supabase/schema.sql b/supabase/schema.sql index cbe5d66..8db1681 100644 --- a/supabase/schema.sql +++ b/supabase/schema.sql @@ -23,7 +23,14 @@ create policy "Anyone can submit a contact message" on public.contact_messages for insert to anon, authenticated - with check (true); + -- Bounded instead of `true`: still an open, insert-only public form, but the + -- payload must be non-empty and within sane sizes (blocks empty/huge spam and + -- satisfies the "RLS policy always true" advisor). + with check ( + char_length(name) between 1 and 200 + and char_length(email) between 3 and 320 + and char_length(message) between 1 and 5000 + ); -- Resume builder (resume.binarysemaphore.com). One row per saved resume; the -- editable document lives in `content` (jsonb, shape = ResumeContent in @@ -83,10 +90,15 @@ create policy "Owners can delete their resumes" on public.resumes for delete to authenticated using (auth.uid() = user_id); --- Keep updated_at current on every write. +-- Keep updated_at current on every write. `security invoker` + an empty +-- search_path pin the function so it can't be hijacked via a mutable search +-- path (resolves the "function search path mutable" advisor). now() lives in +-- pg_catalog, which is always resolved, so no qualification is needed. create or replace function public.set_updated_at() returns trigger language plpgsql +security invoker +set search_path = '' as $$ begin new.updated_at = now();