fix: patch critical auth and RLS security findings from final review
This commit is contained in:
@@ -51,6 +51,24 @@ export async function middleware(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Role-based route enforcement for protected paths
|
||||||
|
if (user && !isPublicRoute && pathname !== '/') {
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('users')
|
||||||
|
.select('role')
|
||||||
|
.eq('id', user.id)
|
||||||
|
.single()
|
||||||
|
|
||||||
|
const role = profile?.role
|
||||||
|
if (isValidRole(role)) {
|
||||||
|
const allowedPrefix = ROLE_HOME[role as UserRole]
|
||||||
|
// Block access to routes that don't belong to this role
|
||||||
|
if (!pathname.startsWith(allowedPrefix)) {
|
||||||
|
return NextResponse.redirect(new URL(allowedPrefix, request.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return supabaseResponse
|
return supabaseResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
-- 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);
|
||||||
Reference in New Issue
Block a user