security: enforce https on dues_payment_methods.url at the data layer
The url renders to members as a clickable href. https was enforced only in the admin Zod action, but RLS lets an admin/board write the row directly via PostgREST, bypassing Zod, with no DB constraint -- a malicious admin could store a javascript: URL and XSS members who click. - Migration 050: CHECK (url ~* '^https://') so the DB is the source of truth; integration-tested that the service path cannot store javascript:/http:. - Member-read action filters with isSafeDuesUrl (defense-in-depth).
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
-- =============================================================================
|
||||
-- 050: dues_payment_methods.url must be an absolute https URL (CHECK constraint)
|
||||
-- =============================================================================
|
||||
-- The url is rendered to MEMBERS as a clickable <a href>. The admin Zod schema
|
||||
-- (duesPaymentMethodSchema / isSafeDuesUrl) enforces https, but RLS lets an
|
||||
-- admin/board write the row directly via PostgREST, bypassing the app-layer
|
||||
-- validation. Without a data-layer constraint a malicious admin could store a
|
||||
-- `javascript:` URL and XSS members who click it. Enforce https at the DB so
|
||||
-- the schema is the single source of truth for what can ever be stored.
|
||||
--
|
||||
-- Case-insensitive prefix match (`~*`) so `HTTPS://` (which new URL() accepts)
|
||||
-- is allowed too; the member-read action additionally re-validates with
|
||||
-- isSafeDuesUrl. Existing prod rows: none yet, so the constraint adds cleanly.
|
||||
--
|
||||
-- Idempotent: only adds the constraint if it is not already present. Apply
|
||||
-- as-is (Supabase SQL editor / psql); re-runs are no-ops.
|
||||
-- =============================================================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'dues_payment_methods_url_https'
|
||||
) THEN
|
||||
ALTER TABLE public.dues_payment_methods
|
||||
ADD CONSTRAINT dues_payment_methods_url_https CHECK (url ~* '^https://');
|
||||
END IF;
|
||||
END $$;
|
||||
+5
-1
@@ -2811,6 +2811,9 @@ CREATE TRIGGER trg_notification_prefs_touch
|
||||
-- Link configuration only, no automated payment record. RLS: SELECT = any
|
||||
-- space member (they render the buttons); writes admin/board. url is validated
|
||||
-- https-only at the server-action boundary.
|
||||
-- url must be an absolute https URL: it is rendered to members as a clickable
|
||||
-- href, and RLS lets admins write the row directly (bypassing the app's Zod
|
||||
-- check), so the data layer is the source of truth (migration 050).
|
||||
CREATE TABLE IF NOT EXISTS public.dues_payment_methods (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
@@ -2821,7 +2824,8 @@ CREATE TABLE IF NOT EXISTS public.dues_payment_methods (
|
||||
sort_order integer NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (space_id, platform)
|
||||
UNIQUE (space_id, platform),
|
||||
CONSTRAINT dues_payment_methods_url_https CHECK (url ~* '^https://')
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_dues_payment_methods_space
|
||||
ON public.dues_payment_methods (space_id);
|
||||
|
||||
Reference in New Issue
Block a user