diff --git a/src/hooks/useChatbot.ts b/src/hooks/useChatbot.ts index d9e7fd57..57019d85 100644 --- a/src/hooks/useChatbot.ts +++ b/src/hooks/useChatbot.ts @@ -3,6 +3,7 @@ import { supabase } from "@/integrations/supabase/client"; import { API_BASE_URL } from "@/config/api"; import { logError } from "@/utils/logger"; import { toast } from "sonner"; +import { sanitizeMessageContent } from "@/utils/sanitize"; export type Message = { role: "user" | "assistant"; @@ -75,7 +76,8 @@ export function useChatbot() { if (!session?.user) return; const userId = session.user.id; - const userMsg: Message = { role: "user", text: input, user_id: userId }; + const sanitizedText = sanitizeMessageContent(input); + const userMsg: Message = { role: "user", text: sanitizedText, user_id: userId }; const updatedMessages = [...messages, userMsg]; @@ -120,7 +122,8 @@ export function useChatbot() { const data = await res.json(); const botReply = data?.answer || "No response 😅"; - const botMsg: Message = { role: "assistant", text: botReply, user_id: userId }; + const sanitizedBotReply = sanitizeMessageContent(botReply); + const botMsg: Message = { role: "assistant", text: sanitizedBotReply, user_id: userId }; // Smoother typing effect (chunked rendering) let currentText = ""; diff --git a/src/hooks/useMessages.ts b/src/hooks/useMessages.ts index 3beb06b9..dd95c378 100644 --- a/src/hooks/useMessages.ts +++ b/src/hooks/useMessages.ts @@ -3,6 +3,7 @@ import { supabase } from "@/integrations/supabase/client"; import { useAwardXP } from "@/hooks/useAwardXP"; import { toast } from "@/hooks/use-toast"; import { logError } from "@/utils/logger"; +import { sanitizeMessageContent } from "@/utils/sanitize"; export type ProfileSummary = { id: string; @@ -577,13 +578,14 @@ export function useMessages( } try { + const sanitizedContent = sanitizeMessageContent(content); const { data, error: insertError } = await supabase .from("messages") .insert({ sender_id: currentUserId, receiver_id: selectedUser.id, - content, - text: content, + content: sanitizedContent, + text: sanitizedContent, }) .select("id,sender_id,receiver_id,content,text,message,created_at,read_at") .single(); diff --git a/src/hooks/useRoomChat.ts b/src/hooks/useRoomChat.ts index 062bc1af..64df6a3a 100644 --- a/src/hooks/useRoomChat.ts +++ b/src/hooks/useRoomChat.ts @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from "react"; import { toast } from "sonner"; import { supabase } from "@/integrations/supabase/client"; import { User } from "@supabase/supabase-js"; +import { sanitizeMessageContent } from "@/utils/sanitize"; export function useRoomChat(id: string | undefined, user: User | null) { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -30,9 +31,10 @@ export function useRoomChat(id: string | undefined, user: User | null) { const handleSendMessage = useCallback(async (newMessage: string) => { if (!newMessage.trim() || !user || !id) return false; + const sanitizedMessage = sanitizeMessageContent(newMessage); // eslint-disable-next-line @typescript-eslint/no-explicit-any const { error } = await supabase.from('study_room_messages' as any).insert([ - { room_id: id, profile_id: user.id, content: newMessage } + { room_id: id, profile_id: user.id, content: sanitizedMessage } ]); if (error) { diff --git a/src/utils/sanitize.ts b/src/utils/sanitize.ts new file mode 100644 index 00000000..9a7b6014 --- /dev/null +++ b/src/utils/sanitize.ts @@ -0,0 +1,41 @@ +import DOMPurify from 'dompurify'; + +/** + * Configuration for DOMPurify sanitization. + * Allows basic text formatting while preventing XSS attacks. + */ +const SANITIZE_CONFIG = { + ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'ol', 'li', 'code', 'pre', 'blockquote'], + ALLOWED_ATTR: [], + KEEP_CONTENT: true, +}; + +/** + * Sanitizes user input to prevent XSS attacks. + * Removes potentially dangerous HTML/JavaScript while preserving basic formatting. + * + * @param input - The raw user input to sanitize + * @returns Sanitized string safe for display + */ +export const sanitizeInput = (input: string): string => { + if (!input || typeof input !== 'string') { + return ''; + } + + // First pass: DOMPurify sanitization + const sanitized = DOMPurify.sanitize(input, SANITIZE_CONFIG); + + // Second pass: Remove any remaining dangerous content + return sanitized.replace(/]*>.*?<\/script>/gi, '').replace(/on\w+\s*=/gi, ''); +}; + +/** + * Sanitizes message content for storage. + * Uses strict configuration to prevent stored XSS. + * + * @param content - Message content to sanitize + * @returns Sanitized content safe for storage and display + */ +export const sanitizeMessageContent = (content: string): string => { + return sanitizeInput(content); +};