Fix security vulnerabilities

This commit is contained in:
gpt-engineer-app[bot]
2025-10-04 01:11:43 +00:00
parent b221c75d4a
commit 756d6a5300
5 changed files with 366 additions and 16 deletions

View File

@@ -1208,12 +1208,21 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
target.style.display = 'none';
const parent = target.parentElement;
if (parent) {
parent.innerHTML = `
<div class="absolute inset-0 flex flex-col items-center justify-center text-destructive text-xs">
<div>⚠️ Image failed to load</div>
<div class="mt-1 font-mono text-xs break-all px-2">${photo.url}</div>
</div>
`;
// Create elements safely using DOM API to prevent XSS
const errorContainer = document.createElement('div');
errorContainer.className = 'absolute inset-0 flex flex-col items-center justify-center text-destructive text-xs';
const errorIcon = document.createElement('div');
errorIcon.textContent = '⚠️ Image failed to load';
const urlDisplay = document.createElement('div');
urlDisplay.className = 'mt-1 font-mono text-xs break-all px-2';
// Use textContent to prevent XSS - it escapes HTML automatically
urlDisplay.textContent = photo.url;
errorContainer.appendChild(errorIcon);
errorContainer.appendChild(urlDisplay);
parent.appendChild(errorContainer);
}
}}
/>

View File

@@ -0,0 +1,123 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { useAuth } from '@/hooks/useAuth';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { useToast } from '@/hooks/use-toast';
import { Monitor, Smartphone, Tablet, Trash2 } from 'lucide-react';
import { format } from 'date-fns';
interface UserSession {
id: string;
device_info: any;
last_activity: string;
created_at: string;
expires_at: string;
session_token: string;
}
export function SessionsTab() {
const { user } = useAuth();
const { toast } = useToast();
const [sessions, setSessions] = useState<UserSession[]>([]);
const [loading, setLoading] = useState(true);
const fetchSessions = async () => {
if (!user) return;
const { data, error } = await supabase
.from('user_sessions')
.select('*')
.eq('user_id', user.id)
.order('last_activity', { ascending: false });
if (error) {
console.error('Error fetching sessions:', error);
} else {
setSessions(data || []);
}
setLoading(false);
};
useEffect(() => {
fetchSessions();
}, [user]);
const revokeSession = async (sessionId: string) => {
const { error } = await supabase
.from('user_sessions')
.delete()
.eq('id', sessionId);
if (error) {
toast({
title: 'Error',
description: 'Failed to revoke session',
variant: 'destructive'
});
} else {
toast({
title: 'Success',
description: 'Session revoked successfully'
});
fetchSessions();
}
};
const getDeviceIcon = (deviceInfo: any) => {
const ua = deviceInfo?.userAgent?.toLowerCase() || '';
if (ua.includes('mobile')) return <Smartphone className="w-4 h-4" />;
if (ua.includes('tablet')) return <Tablet className="w-4 h-4" />;
return <Monitor className="w-4 h-4" />;
};
if (loading) {
return <div className="text-muted-foreground">Loading sessions...</div>;
}
return (
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold">Active Sessions</h3>
<p className="text-sm text-muted-foreground">
Manage your active login sessions across devices
</p>
</div>
{sessions.map((session) => (
<Card key={session.id} className="p-4">
<div className="flex items-start justify-between">
<div className="flex gap-3">
{getDeviceIcon(session.device_info)}
<div>
<div className="font-medium">
{session.device_info?.browser || 'Unknown Browser'}
</div>
<div className="text-sm text-muted-foreground">
Last active: {format(new Date(session.last_activity), 'PPpp')}
</div>
<div className="text-sm text-muted-foreground">
Expires: {format(new Date(session.expires_at), 'PPpp')}
</div>
</div>
</div>
<Button
variant="destructive"
size="sm"
onClick={() => revokeSession(session.id)}
>
<Trash2 className="w-4 h-4 mr-2" />
Revoke
</Button>
</div>
</Card>
))}
{sessions.length === 0 && (
<Card className="p-8 text-center text-muted-foreground">
No active sessions found
</Card>
)}
</div>
);
}

View File

@@ -2038,6 +2038,7 @@ export type Database = {
expires_at: string
id: string
ip_address: unknown | null
ip_address_hash: string | null
last_activity: string
session_token: string
user_agent: string | null
@@ -2049,6 +2050,7 @@ export type Database = {
expires_at?: string
id?: string
ip_address?: unknown | null
ip_address_hash?: string | null
last_activity?: string
session_token: string
user_agent?: string | null
@@ -2060,6 +2062,7 @@ export type Database = {
expires_at?: string
id?: string
ip_address?: unknown | null
ip_address_hash?: string | null
last_activity?: string
session_token?: string
user_agent?: string | null
@@ -2174,6 +2177,10 @@ export type Database = {
Args: Record<PropertyKey, never>
Returns: boolean
}
cleanup_expired_sessions: {
Args: Record<PropertyKey, never>
Returns: undefined
}
extract_cf_image_id: {
Args: { url: string }
Returns: string
@@ -2205,6 +2212,10 @@ export type Database = {
}
Returns: boolean
}
hash_ip_address: {
Args: { ip_text: string }
Returns: string
}
is_moderator: {
Args: { _user_id: string }
Returns: boolean

View File

@@ -1,12 +1,13 @@
import { useState } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Settings, User, Shield, Eye, Bell, MapPin, Download } from 'lucide-react';
import { Settings, User, Shield, Eye, Bell, MapPin, Download, MonitorSmartphone } from 'lucide-react';
import { useAuth } from '@/hooks/useAuth';
import { Navigate } from 'react-router-dom';
import { Header } from '@/components/layout/Header';
import { AccountProfileTab } from '@/components/settings/AccountProfileTab';
import { SecurityTab } from '@/components/settings/SecurityTab';
import { SessionsTab } from '@/components/settings/SessionsTab';
import { PrivacyTab } from '@/components/settings/PrivacyTab';
import { NotificationsTab } from '@/components/settings/NotificationsTab';
import { LocationTab } from '@/components/settings/LocationTab';
@@ -49,6 +50,12 @@ export default function UserSettings() {
icon: Shield,
component: SecurityTab
},
{
id: 'sessions',
label: 'Sessions',
icon: MonitorSmartphone,
component: SessionsTab
},
{
id: 'privacy',
label: 'Privacy',
@@ -91,7 +98,7 @@ export default function UserSettings() {
{/* Settings Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="grid w-full grid-cols-2 lg:grid-cols-6 h-auto p-1">
<TabsList className="grid w-full grid-cols-2 lg:grid-cols-7 h-auto p-1">
{tabs.map((tab) => {
const Icon = tab.icon;
return (

View File

@@ -0,0 +1,200 @@
-- =====================================================
-- CRITICAL SECURITY FIXES - Priority 1 & 2
-- =====================================================
-- =====================================================
-- 1. PREVENT PRIVILEGE ESCALATION
-- =====================================================
-- Function to prevent unauthorized superuser role assignment
CREATE OR REPLACE FUNCTION public.prevent_superuser_escalation()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
-- If trying to grant superuser role
IF NEW.role = 'superuser' THEN
-- Only existing superusers can grant superuser role
IF NOT EXISTS (
SELECT 1 FROM public.user_roles
WHERE user_id = auth.uid()
AND role = 'superuser'
) THEN
RAISE EXCEPTION 'Only superusers can grant the superuser role';
END IF;
END IF;
RETURN NEW;
END;
$$;
-- Apply trigger to user_roles INSERT
DROP TRIGGER IF EXISTS enforce_superuser_escalation_prevention ON public.user_roles;
CREATE TRIGGER enforce_superuser_escalation_prevention
BEFORE INSERT ON public.user_roles
FOR EACH ROW
EXECUTE FUNCTION public.prevent_superuser_escalation();
-- Function to prevent unauthorized modification of superuser roles
CREATE OR REPLACE FUNCTION public.prevent_superuser_role_removal()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
-- If trying to delete a superuser role
IF OLD.role = 'superuser' THEN
-- Only existing superusers can remove superuser roles
IF NOT EXISTS (
SELECT 1 FROM public.user_roles
WHERE user_id = auth.uid()
AND role = 'superuser'
) THEN
RAISE EXCEPTION 'Only superusers can remove the superuser role';
END IF;
END IF;
RETURN OLD;
END;
$$;
-- Apply trigger to user_roles DELETE
DROP TRIGGER IF EXISTS enforce_superuser_removal_prevention ON public.user_roles;
CREATE TRIGGER enforce_superuser_removal_prevention
BEFORE DELETE ON public.user_roles
FOR EACH ROW
EXECUTE FUNCTION public.prevent_superuser_role_removal();
-- Function to audit all role changes
CREATE OR REPLACE FUNCTION public.audit_role_changes()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO public.admin_audit_log (
admin_user_id,
target_user_id,
action,
details
) VALUES (
auth.uid(),
NEW.user_id,
'role_granted',
jsonb_build_object(
'role', NEW.role,
'timestamp', now()
)
);
ELSIF TG_OP = 'DELETE' THEN
INSERT INTO public.admin_audit_log (
admin_user_id,
target_user_id,
action,
details
) VALUES (
auth.uid(),
OLD.user_id,
'role_revoked',
jsonb_build_object(
'role', OLD.role,
'timestamp', now()
)
);
END IF;
RETURN COALESCE(NEW, OLD);
END;
$$;
-- Apply trigger to user_roles
DROP TRIGGER IF EXISTS audit_role_changes_trigger ON public.user_roles;
CREATE TRIGGER audit_role_changes_trigger
AFTER INSERT OR DELETE ON public.user_roles
FOR EACH ROW
EXECUTE FUNCTION public.audit_role_changes();
-- =====================================================
-- 2. RESTRICT PUBLIC PROFILE ACCESS
-- =====================================================
-- Remove overly permissive policy
DROP POLICY IF EXISTS "Public can view basic profile info only" ON public.profiles;
-- New policy: Authenticated users can view profiles
CREATE POLICY "Authenticated users can view profiles"
ON public.profiles
FOR SELECT
TO authenticated
USING (
-- Users can view their own profile completely
(auth.uid() = user_id)
OR
-- Moderators can view all profiles
is_moderator(auth.uid())
OR
-- Others can only view public, non-banned profiles
(privacy_level = 'public' AND NOT banned)
);
-- =====================================================
-- 3. SESSION SECURITY ENHANCEMENTS
-- =====================================================
-- Function to hash IP addresses for privacy
CREATE OR REPLACE FUNCTION public.hash_ip_address(ip_text text)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
-- Use SHA256 hash with salt
RETURN encode(
digest(ip_text || 'thrillwiki_ip_salt_2025', 'sha256'),
'hex'
);
END;
$$;
-- Add hashed IP column if not exists
ALTER TABLE public.user_sessions
ADD COLUMN IF NOT EXISTS ip_address_hash text;
-- Update existing records (hash current IPs)
UPDATE public.user_sessions
SET ip_address_hash = public.hash_ip_address(host(ip_address)::text)
WHERE ip_address IS NOT NULL AND ip_address_hash IS NULL;
-- Function to clean up expired sessions
CREATE OR REPLACE FUNCTION public.cleanup_expired_sessions()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
DELETE FROM public.user_sessions
WHERE expires_at < now();
END;
$$;
-- Allow users to delete their own sessions (for revocation)
DROP POLICY IF EXISTS "Users can delete their own sessions" ON public.user_sessions;
CREATE POLICY "Users can delete their own sessions"
ON public.user_sessions
FOR DELETE
TO authenticated
USING (auth.uid() = user_id);
-- Allow users to view their own sessions
DROP POLICY IF EXISTS "Users can view their own sessions" ON public.user_sessions;
CREATE POLICY "Users can view their own sessions"
ON public.user_sessions
FOR SELECT
TO authenticated
USING (auth.uid() = user_id);