monacousa-portal/supabase/migrations/017_fix_rls_role_escalation...

17 lines
623 B
MySQL
Raw Normal View History

Phase 1: Full implementation — security, bugs, utilities, UI/UX, consolidation 28 items across 7 batches. 36 files changed (9 new, 27 modified). 1061 insertions, 406 deletions. == Batch 1: Critical Security Fixes == 1.1 — Fix open redirect in /auth/callback - src/routes/auth/callback/+server.ts: url.searchParams.get('next') was used directly in redirect(303, next). Attacker could set next=https://evil.com. Now wrapped through sanitizeRedirectUrl() which rejects protocol/host, //, javascript: prefixes; falls back to /dashboard. 1.2 — Fix open redirect in /login - src/routes/(auth)/login/+page.server.ts: redirectTo param used without validation in both load() and form action. Applied sanitizeRedirectUrl() to both locations. 1.3 — Fix RLS self-role-escalation - supabase/migrations/017_fix_rls_role_escalation.sql (NEW): "Users can update own profile" policy had USING(auth.uid()=id) but no WITH CHECK clause — users could SET role='admin' on their own row. Added WITH CHECK constraining role to current value. - deploy/init.sql: updated to match migration 017. 1.4 — Remove hardcoded secrets from docker-compose.yml - docker-compose.yml: removed hardcoded SECRET_KEY_BASE fallback. == Batch 2: Critical & High Bugs == 2.1 — Fix deleteAvatar wrong argument type - src/routes/(app)/settings/+page.server.ts: was passing supabase client object as second arg to deleteAvatar(memberId, avatarPath). Changed to pass member.avatar_url instead. 2.2 — Fix event.start_time typo -> event.start_datetime - src/routes/(app)/board/events/[id]/attendees/+page.server.ts: referenced event.start_time (doesn't exist on type). Caused "Invalid Date" in invitation/roll-call emails. Replaced both occurrences with event.start_datetime. 2.3 — Fix landing page CTA buttons missing href - src/routes/+page.svelte: Sign In and Join Us buttons had no href attribute — completely non-functional for visitors. Added href="/login" and href="/join" respectively. 2.4 — Fix auth pages logo inconsistency - src/routes/auth/reset-password/+page.svelte: hardcoded "M" letter in colored box replaced with actual Monaco USA logo image (MONACOUSA-Flags_376x376.png) matching login/layout. 2.5 — Fix currency USD -> EUR everywhere - src/routes/(app)/board/reports/+page.svelte: USD -> EUR, locale to fr-MC. - src/routes/public/events/[id]/+page.svelte: USD -> EUR, locale to fr-MC. - src/routes/(app)/admin/dashboard/+page.svelte: USD -> EUR, locale to fr-MC. == Batch 3: High Security Fixes == 3.1 — Sanitize HTML in email template rendering - src/lib/server/email.ts: added escapeHtml() utility that escapes &, <, >, ", '. Applied to all template variable values in sendTemplatedEmail() before substitution. URL-type keys (logo_url, site_url) exempted. Prevents XSS in emails. 3.2 — Add file upload MIME type validation - src/lib/server/storage.ts: added MAGIC_BYTES constant and validateFileMagicBytes() function checking PNG (89504E47), JPEG (FFD8FF), PDF (25504446), WebP (52494646), GIF (47494638) magic bytes against declared MIME. Applied in uploadAvatar and uploadDocument before storing. 3.3 — Docker container hardening - docker-compose.yml portal service: added security_opt [no-new-privileges:true], read_only: true with tmpfs for /tmp, deploy.resources.limits (memory: 512M, cpus: 1.0). Dockerfile already had USER sveltekit (non-root). 3.4 — Restrict board endpoints data exposure - src/routes/(app)/board/members/+page.server.ts: replaced .select('*') with explicit column list returning only fields the board UI actually displays. Removed sensitive columns. == Batch 4: Shared Utilities == 4.1 — Extract getVisibleLevels to shared utility - src/lib/server/visibility.ts (NEW): exports getVisibleLevels(role) returning appropriate visibility levels per role. - Replaced 4 duplicate definitions in: src/routes/(app)/dashboard/+page.server.ts src/routes/(app)/documents/+page.server.ts src/routes/(app)/events/+page.server.ts src/routes/(app)/events/[id]/+page.server.ts 4.3 — Fix N+1 query in getReminderEffectiveness - src/lib/server/dues.ts: rewrote loop executing individual DB queries per reminder into single batch query with IN filter. Maps results in JS instead of N+1 round-trips. == Batch 5: Shared UI Components == 5.1 — Create reusable EmptyState component - src/lib/components/ui/empty-state.svelte (NEW): accepts icon, title, description props and optional children snippet. Consistent muted-text centered layout matching design system. - Applied in DocumentPreviewModal and NotificationCenter. 5.2 — Move LoadingSpinner to shared ui/ - src/lib/components/ui/LoadingSpinner.svelte (NEW): copied from auth/ to ui/ for general use. Original kept for compatibility. - src/lib/components/ui/index.ts: added barrel exports for EmptyState and LoadingSpinner. == Batch 6: UX Standardization == 6.4 — Add skip-to-content link - src/routes/(app)/+layout.svelte: added visually-hidden-until- focused skip link as first focusable element: <a href="#main-content" class="sr-only focus:not-sr-only ..."> Added id="main-content" to <main> element. 6.5 — Add navigation loading indicator - src/routes/(app)/+layout.svelte: imported SvelteKit $navigating store. Shows thin animated progress bar at page top during transitions. CSS-only animation, no external dependencies. == Batch 7: Code Consolidation == 7.1 — Consolidate profile/settings pages - src/lib/server/member-profile.ts (NEW, 283 lines): shared helpers handleAvatarUpload(), handleAvatarRemoval(), handleProfileUpdate(). Supports admin mode (supabaseAdmin) and user mode (scoped client). - src/routes/(app)/profile/+page.server.ts: simplified from ~167 to ~88 lines using shared helpers. - src/routes/(app)/settings/+page.server.ts: simplified from ~219 to ~106 lines using shared helpers. 7.2 — Consolidate registration flows - src/lib/server/registration.ts (NEW, 201 lines): shared helpers createMemberRecord(), cleanupAuthUser(), sendWelcomeEmail(). - src/routes/(auth)/signup/+page.server.ts: simplified from ~167 to ~85 lines using shared helpers. - src/routes/join/+page.server.ts: simplified from ~209 to ~117 lines using shared helpers. 7.3 — Create status badge utility - src/lib/utils/status-badges.ts (NEW, 55 lines): centralized STATUS_MAP for all status types (membership, dues, payment, RSVP, event, roles). Exports getStatusConfig(), getStatusBadgeClasses(), getStatusLabel(). 7.4 — Create rate limiting utility - src/lib/server/rate-limit.ts (NEW, 73 lines): in-memory Map-based rate limiter with TTL cleanup. Exports checkRateLimit(key, maxAttempts, windowMs) and resetRateLimit(). - Applied to login: 5 attempts per 15 min by email. - Applied to forgot-password: 3 attempts per 15 min by email. - src/routes/(auth)/login/+page.server.ts: added rate limit check before signInWithPassword, reset on success. - src/routes/(auth)/forgot-password/+page.server.ts: added rate limit check before resetPasswordForEmail. == New Files (9) == src/lib/server/auth-utils.ts src/lib/server/visibility.ts src/lib/server/member-profile.ts src/lib/server/registration.ts src/lib/server/rate-limit.ts src/lib/server/email.ts (escapeHtml addition) src/lib/server/storage.ts (validateFileMagicBytes addition) src/lib/utils/status-badges.ts src/lib/components/ui/empty-state.svelte src/lib/components/ui/LoadingSpinner.svelte supabase/migrations/017_fix_rls_role_escalation.sql Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 07:54:10 +01:00
-- Migration 017: Fix RLS policy to prevent self-role-escalation
-- The "Users can update own profile" policy allows users to SET role = 'admin'
-- on their own row because it lacks a WITH CHECK clause restricting role changes.
-- Drop the existing policy
DROP POLICY IF EXISTS "Users can update own profile" ON public.members;
-- Recreate with WITH CHECK that prevents role changes
CREATE POLICY "Users can update own profile"
ON public.members FOR UPDATE
TO authenticated
USING (auth.uid() = id)
WITH CHECK (
auth.uid() = id
AND role = (SELECT m.role FROM public.members m WHERE m.id = auth.uid())
);