Security: Add XSS sanitization to chat messages - #1877
Conversation
Implement input sanitization for all message types to prevent stored XSS attacks: - Add sanitize.ts utility module using DOMPurify for consistent sanitization - Sanitize user messages before storing in useMessages.ts - Sanitize room chat messages in useRoomChat.ts - Sanitize chatbot messages in useChatbot.ts This ensures malicious content cannot be stored in the database or rendered to other users, protecting against stored XSS vulnerabilities. Fixes durdana3105#1874 Signed-off-by: Anshul Jain <anshul23102@iiitd.ac.in>
|
@anshul23102 is attempting to deploy a commit to the durdana3105's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@durdana3105 Thank you for reviewing! This PR implements input sanitization for all chat message types to prevent stored XSS attacks. All user input is sanitized using DOMPurify before being stored in the database. Could you please review and consider merging if it looks good? Also, when ready, please add the Let me know if any changes are needed! |
📝 WalkthroughWalkthroughAdds shared DOMPurify-based sanitization for chat content and applies it to user, assistant, threaded, and study-room messages before persistence. ChangesChat message sanitization
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with 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.
Inline comments:
In `@src/hooks/useChatbot.ts`:
- Around line 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.
In `@src/hooks/useMessages.ts`:
- Around line 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.
In `@src/utils/sanitize.ts`:
- Around line 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.
- Around line 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.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 04032ab7-f6f4-4812-85e1-102ae6054ddd
📒 Files selected for processing (4)
src/hooks/useChatbot.tssrc/hooks/useMessages.tssrc/hooks/useRoomChat.tssrc/utils/sanitize.ts
| const sanitizedBotReply = sanitizeMessageContent(botReply); | ||
| const botMsg: Message = { role: "assistant", text: sanitizedBotReply, user_id: userId }; |
There was a problem hiding this comment.
🔒 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 -lRepository: 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 -200Repository: 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: chunksanitizedBotReplyfor the typewriter instead of rawbotReply.src/hooks/useMessages.ts: sanitize RPC summary rows, loaded/paginated messages, and realtime rows before updatingthreadMessages/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-L588src/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.
| 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, |
There was a problem hiding this comment.
🎯 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 emptysanitizedContentbefore insertion and XP award.src/hooks/useChatbot.ts#L79-L80: reject emptysanitizedTextbefore state, API, and persistence.src/hooks/useRoomChat.ts#L34-L37: reject emptysanitizedMessagebefore insertion.
📍 Affects 3 files
src/hooks/useMessages.ts#L581-L588(this comment)src/hooks/useChatbot.ts#L79-L80src/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.
| 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, ''); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
rg -n 'react-markdown|remark|marked|dangerouslySetInnerHTML|innerHTML' srcRepository: 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'; fiRepository: 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'; fiRepository: 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' || trueRepository: 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 400Repository: 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]}")
PYRepository: 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.
Summary
Implement input sanitization for chat messages to prevent stored XSS attacks.
Changes
Security Impact
Prevents stored XSS attacks in message content.
Closes #1874
Summary by CodeRabbit