Files
ims/supabase/migrations/20260709000009_security_fixes.sql
T

114 lines
3.9 KiB
PL/PgSQL

-- supabase/migrations/20260709000009_security_fixes.sql
-- Security fixes from IMS Phase 0 final code review
-- =============================================================================
-- 1. CRITICAL: Drop and recreate users_update_own with WITH CHECK to prevent
-- role self-escalation (any user setting their own role to admin)
-- =============================================================================
DROP POLICY IF EXISTS "users_update_own" ON users;
CREATE POLICY "users_update_own" ON users
FOR UPDATE
USING (id = auth.uid())
WITH CHECK (
id = auth.uid()
AND role = (SELECT role FROM public.users WHERE id = auth.uid())
);
-- =============================================================================
-- 2. IMPORTANT: SECURITY DEFINER audit writer function — app code calls this
-- instead of direct INSERT on audit_log
-- =============================================================================
CREATE OR REPLACE FUNCTION public.write_audit_log(
p_table_name TEXT,
p_record_id UUID,
p_action TEXT,
p_old_value JSONB DEFAULT NULL,
p_new_value JSONB DEFAULT NULL
)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
INSERT INTO public.audit_log (table_name, record_id, action, changed_by, old_value, new_value)
VALUES (p_table_name, p_record_id, p_action, auth.uid(), p_old_value, p_new_value);
END;
$$;
-- =============================================================================
-- 3. IMPORTANT: notifications_log INSERT policy for elevated roles
-- =============================================================================
CREATE POLICY "notifications_insert_elevated" ON notifications_log
FOR INSERT
WITH CHECK (auth_user_role() IN ('hse', 'admin', 'supervisor'));
-- =============================================================================
-- 4. IMPORTANT: Recreate generate_incident_reference trigger function with
-- SECURITY DEFINER and SET search_path to prevent search_path injection
-- =============================================================================
CREATE OR REPLACE FUNCTION public.generate_incident_reference()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_site_code TEXT;
v_month TEXT;
v_seq INT;
BEGIN
SELECT UPPER(REGEXP_REPLACE(SUBSTRING(name, 1, 6), '[^A-Za-z0-9]', '', 'g'))
INTO v_site_code
FROM sites
WHERE id = NEW.site_id;
v_month := TO_CHAR(NEW.reported_at, 'YYYYMM');
SELECT COUNT(*) + 1
INTO v_seq
FROM incidents
WHERE site_id = NEW.site_id
AND TO_CHAR(reported_at, 'YYYYMM') = v_month;
NEW.reference_no := COALESCE(v_site_code, 'UNK') || '-' || v_month || '-' || LPAD(v_seq::TEXT, 4, '0');
RETURN NEW;
END;
$$;
-- =============================================================================
-- 5. IMPORTANT: Recreate helper functions with SET search_path to prevent
-- search_path hijacking attacks
-- =============================================================================
CREATE OR REPLACE FUNCTION public.auth_user_role()
RETURNS user_role
LANGUAGE sql
SECURITY DEFINER
STABLE
SET search_path = public
AS $$ SELECT role FROM public.users WHERE id = auth.uid() $$;
CREATE OR REPLACE FUNCTION public.auth_user_site_id()
RETURNS UUID
LANGUAGE sql
SECURITY DEFINER
STABLE
SET search_path = public
AS $$ SELECT site_id FROM public.users WHERE id = auth.uid() $$;
CREATE OR REPLACE FUNCTION public.auth_user_department()
RETURNS TEXT
LANGUAGE sql
SECURITY DEFINER
STABLE
SET search_path = public
AS $$ SELECT department FROM public.users WHERE id = auth.uid() $$;
-- =============================================================================
-- 6. IMPORTANT: Add UNIQUE constraint to dosh_reports to prevent duplicate
-- form submissions for the same incident
-- =============================================================================
ALTER TABLE dosh_reports
ADD CONSTRAINT dosh_reports_incident_form_uq UNIQUE (incident_id, form_type);