Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/hooks/useChatbot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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];

Expand Down Expand Up @@ -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 };
Comment on lines +125 to +126

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg '(^|/)(useChatbot|useMessages|useRoomChat)\.ts$|sanitizeMessageContent' || true

echo
echo "== file sizes =="
for f in src/hooks/useChatbot.ts src/hooks/useMessages.ts src/hooks/useRoomChat.ts; do
  [ -f "$f" ] && { echo "$f"; wc -l "$f"; }
done

echo
echo "== locate sanitizeMessageContent definitions/usages =="
rg -n "function sanitizeMessageContent|const sanitizeMessageContent|sanitizeMessageContent" src/hooks src || true

echo
echo "== relevant outlines =="
for f in src/hooks/useChatbot.ts src/hooks/useMessages.ts src/hooks/useRoomChat.ts; do
  [ -f "$f" ] && { echo "--- $f"; ast-grep outline "$f" || true; }
done

echo
echo "== focused source snippets =="
for f in src/hooks/useChatbot.ts src/hooks/useMessages.ts src/hooks/useRoomChat.ts; do
  echo "--- $f lines 50-150"
  sed -n '50,150p' "$f" | cat -n | sed 's/^/  /'
done

echo "--- useMessages around lines 540-620"
sed -n '540,620p' src/hooks/useMessages.ts | cat -l

echo "--- useRoomChat around lines 1-70"
sed -n '1,70p' src/hooks/useRoomChat.ts | cat -l

Repository: durdana3105/peer-learning

Length of output: 11807


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- src/hooks/useMessages.ts lines 90-180"
sed -n '90,180p' src/hooks/useMessages.ts

echo
echo "--- src/hooks/useMessages.ts lines 200-340"
sed -n '200,340p' src/hooks/useMessages.ts

echo
echo "--- src/hooks/useMessages.ts lines 460-615"
sed -n '460,615p' src/hooks/useMessages.ts

echo
echo "--- src/hooks/useRoomChat.ts lines 1-70"
sed -n '1,70p' src/hooks/useRoomChat.ts

echo
echo "--- src/utils/sanitize.ts lines 1-120"
sed -n '1,120p' src/utils/sanitize.ts

echo
echo "== render boundary searches =="
rg -n "rawSummaries|conversationSummaries|threadMessages|chat_messages|study_room_messages|sanitizeMessageContent|rawSummaryToMessage|setRawSummaries|setThreadMessages" src/hooks src --glob '*.ts' --glob '*.tsx'

Repository: durdana3105/peer-learning

Length of output: 26385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- src/pages/Messages.tsx lines 1-220"
sed -n '1,220p' src/pages/Messages.tsx

echo
echo "--- src/components/messages/Sidebar.tsx lines 170-210"
sed -n '170,210p' src/components/messages/Sidebar.tsx

echo
echo "--- src/components/messages/ChatWindow.tsx lines 180-260"
sed -n '180,260p' src/components/messages/ChatWindow.tsx

echo
echo "--- locate Chatbot use/render and room chat usage/render =="
rg -n "useChatbot\\(|Chatbot|useRoomChat\\(|RoomChat|messages" src/pages src/components --glob '*.tsx' --glob '*.ts' | head -200

Repository: durdana3105/peer-learning

Length of output: 18834


Sanitize every message read before hydration or rendering.

New inserts protect the current client write path, but legacy/bypassed content can still reach the UI before it’s saved or streamed.

  • src/hooks/useChatbot.ts#L17: sanitize loaded chat history before state.
  • src/hooks/useChatbot.ts#L85-L96: chunk sanitizedBotReply for the typewriter instead of raw botReply.
  • src/hooks/useMessages.ts: sanitize RPC summary rows, loaded/paginated messages, and realtime rows before updating threadMessages/conversation summaries.
  • src/hooks/useRoomChat.ts#L17: sanitize the fetched room message rows before injecting them into render state.
📍 Affects 3 files
  • src/hooks/useChatbot.ts#L125-L126 (this comment)
  • src/hooks/useMessages.ts#L581-L588
  • src/hooks/useRoomChat.ts#L34-L37
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useChatbot.ts` around lines 125 - 126, Sanitize all message content
before hydration, streaming, or rendering: in src/hooks/useChatbot.ts at lines
125-126 and 85-96, sanitize loaded history and use sanitizedBotReply for
typewriter chunks and bot message state; in src/hooks/useMessages.ts at lines
581-588, sanitize RPC summaries, loaded/paginated messages, and realtime rows
before updating threadMessages or conversation summaries; in
src/hooks/useRoomChat.ts at lines 34-37, sanitize fetched room message rows
before placing them in render state.


// Smoother typing effect (chunked rendering)
let currentText = "";
Expand Down
6 changes: 4 additions & 2 deletions src/hooks/useMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Comment on lines +581 to +588

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate the sanitized value before persisting.

Raw non-empty checks allow content consisting only of removable markup to become empty messages.

  • src/hooks/useMessages.ts#L581-L588: reject empty sanitizedContent before insertion and XP award.
  • src/hooks/useChatbot.ts#L79-L80: reject empty sanitizedText before state, API, and persistence.
  • src/hooks/useRoomChat.ts#L34-L37: reject empty sanitizedMessage before insertion.
📍 Affects 3 files
  • src/hooks/useMessages.ts#L581-L588 (this comment)
  • src/hooks/useChatbot.ts#L79-L80
  • src/hooks/useRoomChat.ts#L34-L37
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useMessages.ts` around lines 581 - 588, Validate the sanitized
content before any side effects: in src/hooks/useMessages.ts lines 581-588,
reject empty sanitizedContent before insertion and XP awarding; in
src/hooks/useChatbot.ts lines 79-80, reject empty sanitizedText before state
updates, API calls, or persistence; and in src/hooks/useRoomChat.ts lines 34-37,
reject empty sanitizedMessage before insertion. Use the existing message-sending
functions and preserve behavior for non-empty sanitized values.

})
.select("id,sender_id,receiver_id,content,text,message,created_at,read_at")
.single();
Expand Down
4 changes: 3 additions & 1 deletion src/hooks/useRoomChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
41 changes: 41 additions & 0 deletions src/utils/sanitize.ts
Original file line number Diff line number Diff line change
@@ -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[^>]*>.*?<\/script>/gi, '').replace(/on\w+\s*=/gi, '');
Comment on lines +20 to +29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

rg -n 'react-markdown|remark|marked|dangerouslySetInnerHTML|innerHTML' src

Repository: durdana3105/peer-learning

Length of output: 483


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- files ---\n'
git ls-files | sed -n '1,120p'

printf '\n--- MarkdownRenderer.tsx ---\n'
cat -n src/components/MarkdownRenderer.tsx

printf '\n--- sanitize.ts ---\n'
cat -n src/utils/sanitize.ts

printf '\n--- usages of sanitizeInput and MarkdownRenderer ---\n'
rg -n 'sanitizeInput|MarkdownRenderer' src

printf '\n--- package react markdown versions ---\n'
if [ -f package.json ]; then cat package.json | sed -n '1,220p'; fi

Repository: durdana3105/peer-learning

Length of output: 254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- files ---'
git ls-files | sed -n '1,160p'

echo
echo '--- MarkdownRenderer.tsx ---'
cat -n src/components/MarkdownRenderer.tsx

echo
echo '--- sanitize.ts ---'
cat -n src/utils/sanitize.ts

echo
echo '--- usages of sanitizeInput and MarkdownRenderer ---'
rg -n 'sanitizeInput|MarkdownRenderer' src

echo
echo '--- package react markdown versions ---'
if [ -f package.json ]; then cat package.json | sed -n '1,220p'; fi

Repository: durdana3105/peer-learning

Length of output: 13847


Sanitize after Markdown/HTML transformation.

sanitizeInput runs before ReactMarkdown, and Markdown can render raw HTML, including img tags and src/on* attributes. Sanitize the rendered HTML in MarkdownRenderer by adding a strict rehype-sanitize configuration that only allows safe text-formatting tags and strips risky attributes/schemes (src, href, on*).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/sanitize.ts` around lines 20 - 29, Update MarkdownRenderer to
sanitize the HTML produced by ReactMarkdown using rehype-sanitize with a strict
schema that allows only safe text-formatting tags and excludes risky attributes
and URL schemes, including src, href, and on* handlers. Keep sanitizeInput for
its existing preprocessing role, but ensure rendered Markdown cannot emit raw
unsafe HTML.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | sed -n '1,160p'

echo "== sanitize paths =="
fd -i 'sanitize|supabase|rls|policy|rds|function|rpc|edge|trigger' . | sed -n '1,160p'

echo "== sanitize.ts =="
if [ -f src/utils/sanitize.ts ]; then
  nl -ba src/utils/sanitize.ts | sed -n '1,120p'
fi

echo "== usages of sanitizeInput and sanitizeText =="
rg -n "sanitizeInput|sanitizeText|SANITIZE_CONFIG|DOMPurify" . -g '!node_modules' -g '!dist' -g '!build' || true

echo "== supabase/client writes in tracked code =="
rg -n "supabase|from('supabase|Supabase|createClient|rpc\\(|insert\\(|upsert|update\\(|delete\\(" . -g '!node_modules' -g '!dist' -g '!build' | sed -n '1,240p'

Repository: durdana3105/peer-learning

Length of output: 6773


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate sanitize.ts and supabase files =="
fd -i 'sanitize\.ts|supabase\.ts(js)?|supabase' src supabase backend 2>/dev/null | sed -n '1,160p' || true

echo "== src/utils/sanitize.ts =="
if [ -f src/utils/sanitize.ts ]; then
  cat -n src/utils/sanitize.ts | sed -n '1,140p'
fi

echo "== supabase migration files mentioning insert/read/update/policy/default/database function =="
python3 - <<'PY'
from pathlib import Path
terms=['insert','read','update','delete','drop policy','create policy','alter policy','security invariant','security definer','database function','set search_path','default', 'sanitize']
patterns=['*chat*','*messages*','*rls*','*supabase*','*function*','*.sql']
for p in Path('supabase/migrations').glob('*.sql'):
    text=p.read_text(errors='ignore').lower()
    if any(t in text for t in terms):
        print(f"\n--- {p} ---")
        for term in terms:
            if term in text:
                print(f"contains {term}")
                idx=text.find(term)
                start=text.rfind('\n',0,max(0,idx-200))+1
                if start<0: start=0
                print(text[start:idx+300].split('\n',1)[0:])
PY

echo "== supabase folder outline =="
find supabase/src -maxdepth 3 -type f 2>/dev/null | sort | sed -n '1,200p' || true

echo "== sanitize usages =="
rg -n "sanitizeInput|sanitizeText|SANITIZE_CONFIG|DOMPurify" . -g '!node_modules' -g '!dist' -g '!build' || true

Repository: durdana3105/peer-learning

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== sanitizeInput usages with adjacent code =="
while IFS= read -r f; do
  line=$(rg -n "sanitizeInput" "$f" | cut -d: -f1 | head -n1 || true)
  if [ -n "$line" ]; then
    start=$(( line > 15 ? line - 15 : 1 ))
    end=$(( line + 30 ))
    echo "\n--- $f:$start-$end ---"
    cat -n "$f" | sed -n "${start},$((end+5))p"
  fi
done < <(rg -l "sanitizeInput" . -g '!node_modules' -g '!dist' -g '!build')

echo "== sanitizeMessageContent usages =="
while IFS= read -r f; do
  echo "\n--- $f ---"
  cat -n "$f" | rg -n -C 8 "sanitizeMessageContent|\sanitizeInput|content|content:|messages" || true
done < <(rg -l "sanitizeMessageContent" . -g '!node_modules' -g '!dist' -g '!build')

echo "== messages table RLS and triggers/functions focused =="
rg -n "messages|messages_|content|trunc|regex|regexp_replace|pg_catalog|create.*function|security definer|set search_path|alter table public.messages" supabase/migrations -g '*.sql' --max-count 400

Repository: durdana3105/peer-learning

Length of output: 262


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== sanitize file list =="
rg -l "sanitizeInput|sanitizeMessageContent|SANITIZE_CONFIG|DOMPurify" . -g '!node_modules' -g '!dist' -g '!build' || true

echo "== sanitizeInput usages with context =="
for f in $(rg -l "sanitizeInput|sanitizeMessageContent" . -g '!node_modules' -g '!dist' -g '!build'); do
  echo "\n--- $f ---"
  rg -n -C 12 "sanitizeInput|sanitizeMessageContent" "$f" || true
done

echo "== messages table migration snippets =="
python3 - <<'PY'
from pathlib import Path
for p in sorted(Path('supabase/migrations').glob('*.sql')):
    text = p.read_text(errors='ignore')
    if 'messages' in text.lower() and p.name not in ['20260722000000_add_is_pinned_to_messages.sql']:
        print(f"\n--- {p.name} ---")
        for i,line in enumerate(text.splitlines(), 1):
            if any(kw in line.lower() for kw in ['create table','alter table','create policy','regexp_replace','truncate','pg_catalog','content', 'trigger', 'function']):
                print(f"{i}: {line[:320]}")
PY

Repository: durdana3105/peer-learning

Length of output: 24428


Enforce sanitization on the database insert path.

sanitizeMessageContent() only sanitizes in the browser before Supabase inserts into messages, chat_messages, and study_room_messages; those messages table policies allow authenticated inserts. A user can bypass this utility via the Supabase API / developer tools and store XSS in the database. Move sanitization/validation to a trusted server side such as an RPC/Edge Function, backend controller, or database check/trigge, and keep render-time sanitization for existing legacy rows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/sanitize.ts` around lines 20 - 29, Enforce message-content
sanitization and validation on the trusted server-side insert path for messages,
chat_messages, and study_room_messages, rather than relying on client-side
sanitizeMessageContent or sanitizeInput. Implement this in the existing RPC,
Edge Function, backend controller, or database trigger/check handling those
inserts, while retaining render-time sanitization for legacy rows.

};

/**
* 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);
};
Loading