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:
Moheeb Zara
2026-05-19 21:35:49 -07:00
parent 5ce6b29d4a
commit f2534323da
7 changed files with 61 additions and 2 deletions
+6 -1
View File
@@ -5,6 +5,7 @@ import { createClient } from '@/lib/supabase/server'
import { requireMember, requireMemberWithRole, parseInput } from '@/lib/auth-helpers'
import { ADMIN_ROLES } from '@/lib/permissions'
import { duesPaymentMethodSchema } from '@/lib/validations'
import { isSafeDuesUrl } from '@/lib/dues-payments-logic'
export type DuesPaymentMethod = {
platform: string
@@ -32,7 +33,11 @@ export async function listActiveDuesPaymentMethods(): Promise<
.eq('is_active', true)
.order('sort_order', { ascending: true })
return { data: (data ?? []).map(toMethod) }
// Defense-in-depth: only render absolute-https links. The DB CHECK and the
// admin Zod schema already enforce this, but a member's browser must never
// be handed an unsafe href, so re-validate at the boundary that feeds the
// clickable card and drop anything that is not a safe https URL.
return { data: (data ?? []).map(toMethod).filter(m => isSafeDuesUrl(m.url)) }
}
// Admin-facing: every configured method (active + inactive) for the management