Skip to content

fix(security): sanitize HTML in chat UI to prevent XSS injection#3820

Open
xr843 wants to merge 2 commits intolm-sys:mainfrom
xr843:fix/xss-html-injection
Open

fix(security): sanitize HTML in chat UI to prevent XSS injection#3820
xr843 wants to merge 2 commits intolm-sys:mainfrom
xr843:fix/xss-html-injection

Conversation

@xr843
Copy link

@xr843 xr843 commented Mar 21, 2026

Summary

  • Escape HTML entities in user messages and model responses in Conversation.to_gradio_chatbot() using html.escape(), preventing raw HTML/JS from being rendered in the chat UI
  • Escape dynamic attributes (URL, filetype) in programmatically constructed <img> tags for uploaded images
  • Add sanitize_html=True to all 6 gr.Chatbot() instances across all Gradio UI modules as defense-in-depth

Vulnerability

Both user input and model output were passed unescaped to Gradio's Chatbot component. This allowed:

  1. Direct injection: A user could type <img onerror=alert(1) src=#> and it would execute JavaScript
  2. Model-driven injection: A model could be prompted to return HTML that executes scripts in the viewer's browser
  3. Stored XSS in arena: In battle/arena modes, injected HTML could affect other users viewing shared conversations

Reproduction (from #88):

concatenate "<img src='' onerror=javascript:alert(1)" and "/>"

Approach

The fix applies output escaping (not input sanitization), which is the correct strategy because:

  • Input sanitization doesn't work here since models transform input unpredictably
  • The model itself can generate malicious HTML regardless of input filtering
  • Escaping at the rendering boundary catches all vectors

Intentionally constructed <img> tags for user-uploaded images are preserved since they use trusted data (image objects from Gradio's upload handler), but their dynamic attributes are also escaped as a precaution.

Files changed

File Change
fastchat/conversation.py Add html.escape() to text messages in to_gradio_chatbot()
fastchat/serve/gradio_web_server.py Add sanitize_html=True to gr.Chatbot()
fastchat/serve/gradio_block_arena_anony.py Add sanitize_html=True to gr.Chatbot()
fastchat/serve/gradio_block_arena_named.py Add sanitize_html=True to gr.Chatbot()
fastchat/serve/gradio_block_arena_vision.py Add sanitize_html=True to gr.Chatbot()
fastchat/serve/gradio_block_arena_vision_anony.py Add sanitize_html=True to gr.Chatbot()
fastchat/serve/gradio_block_arena_vision_named.py Add sanitize_html=True to gr.Chatbot()

Test plan

  • Start the Gradio web server and type <img onerror=alert(1) src=#> — should display as plain text, not execute
  • Type <script>alert('xss')</script> — should display as plain text
  • Upload an image and chat — image should still render correctly
  • Verify LaTeX rendering still works (e.g. $E=mc^2$)
  • Test arena mode (both anonymous and named) with HTML input

Fixes #3154
Fixes #88

🤖 Generated with Claude Code

xr843 and others added 2 commits March 21, 2026 17:06
Escape HTML entities in user messages and model responses before
rendering in Gradio chatbot to prevent HTML/XSS injection attacks.

Changes:
- Add html.escape() to all text messages in Conversation.to_gradio_chatbot()
  for both user input (even indices) and model output (odd indices)
- Escape image URL and filetype attributes in constructed img tags
- Add sanitize_html=True to all gr.Chatbot() instances as defense-in-depth

The vulnerability allowed arbitrary HTML/JavaScript execution when:
1. A user typed HTML tags (e.g. <img onerror=alert(1) src=#>) as input
2. A model returned HTML in its response

Fixes lm-sys#3154
Fixes lm-sys#88

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTML injection of the web prompt: possible safety issue? UI contains cross-site scripting (XSS) vulnerabilities

1 participant