From dcdc46ed8e4ab59d6a798ca9b032521227856e57 Mon Sep 17 00:00:00 2001 From: xianren Date: Sat, 21 Mar 2026 17:06:54 +0800 Subject: [PATCH 1/2] fix(security): sanitize HTML in chat UI to prevent XSS injection 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. ) 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) --- fastchat/conversation.py | 14 +++++++++++--- fastchat/serve/gradio_block_arena_anony.py | 1 + fastchat/serve/gradio_block_arena_named.py | 1 + fastchat/serve/gradio_block_arena_vision.py | 1 + fastchat/serve/gradio_block_arena_vision_anony.py | 1 + fastchat/serve/gradio_block_arena_vision_named.py | 1 + fastchat/serve/gradio_web_server.py | 1 + 7 files changed, 17 insertions(+), 3 deletions(-) diff --git a/fastchat/conversation.py b/fastchat/conversation.py index 4a46103ec..ac30eba01 100644 --- a/fastchat/conversation.py +++ b/fastchat/conversation.py @@ -8,6 +8,7 @@ import base64 import dataclasses from enum import auto, IntEnum +import html from io import BytesIO import os from typing import List, Any, Dict, Union, Tuple @@ -370,13 +371,20 @@ def to_gradio_chatbot(self): msg, images = msg image = images[0] # Only one image on gradio at one time if image.image_format == ImageFormat.URL: - img_str = f'user upload image' + img_str = f'user upload image' elif image.image_format == ImageFormat.BYTES: - img_str = f'user upload image' - msg = img_str + msg.replace("\n", "").strip() + img_str = f'user upload image' + msg = img_str + html.escape( + msg.replace("\n", "").strip() + ) + else: + if msg is not None: + msg = html.escape(msg) ret.append([msg, None]) else: + if msg is not None: + msg = html.escape(msg) ret[-1][-1] = msg return ret diff --git a/fastchat/serve/gradio_block_arena_anony.py b/fastchat/serve/gradio_block_arena_anony.py index 625c69c44..2c87530b0 100644 --- a/fastchat/serve/gradio_block_arena_anony.py +++ b/fastchat/serve/gradio_block_arena_anony.py @@ -480,6 +480,7 @@ def build_side_by_side_ui_anony(models): elem_id="chatbot", height=650, show_copy_button=True, + sanitize_html=True, latex_delimiters=[ {"left": "$", "right": "$", "display": False}, {"left": "$$", "right": "$$", "display": True}, diff --git a/fastchat/serve/gradio_block_arena_named.py b/fastchat/serve/gradio_block_arena_named.py index 2f7b39adb..55b5940b5 100644 --- a/fastchat/serve/gradio_block_arena_named.py +++ b/fastchat/serve/gradio_block_arena_named.py @@ -358,6 +358,7 @@ def build_side_by_side_ui_named(models): elem_id=f"chatbot", height=650, show_copy_button=True, + sanitize_html=True, latex_delimiters=[ {"left": "$", "right": "$", "display": False}, {"left": "$$", "right": "$$", "display": True}, diff --git a/fastchat/serve/gradio_block_arena_vision.py b/fastchat/serve/gradio_block_arena_vision.py index b3d812220..aa05ed6bc 100644 --- a/fastchat/serve/gradio_block_arena_vision.py +++ b/fastchat/serve/gradio_block_arena_vision.py @@ -356,6 +356,7 @@ def build_single_vision_language_model_ui( label="Scroll down and start chatting", height=650, show_copy_button=True, + sanitize_html=True, latex_delimiters=[ {"left": "$", "right": "$", "display": False}, {"left": "$$", "right": "$$", "display": True}, diff --git a/fastchat/serve/gradio_block_arena_vision_anony.py b/fastchat/serve/gradio_block_arena_vision_anony.py index d4d4d484e..c77897d59 100644 --- a/fastchat/serve/gradio_block_arena_vision_anony.py +++ b/fastchat/serve/gradio_block_arena_vision_anony.py @@ -432,6 +432,7 @@ def build_side_by_side_vision_ui_anony(context: Context, random_questions=None): elem_id="chatbot", height=650, show_copy_button=True, + sanitize_html=True, latex_delimiters=[ {"left": "$", "right": "$", "display": False}, {"left": "$$", "right": "$$", "display": True}, diff --git a/fastchat/serve/gradio_block_arena_vision_named.py b/fastchat/serve/gradio_block_arena_vision_named.py index 7c653acf3..49f48708b 100644 --- a/fastchat/serve/gradio_block_arena_vision_named.py +++ b/fastchat/serve/gradio_block_arena_vision_named.py @@ -372,6 +372,7 @@ def build_side_by_side_vision_ui_named(context: Context, random_questions=None): elem_id=f"chatbot", height=650, show_copy_button=True, + sanitize_html=True, latex_delimiters=[ {"left": "$", "right": "$", "display": False}, {"left": "$$", "right": "$$", "display": True}, diff --git a/fastchat/serve/gradio_web_server.py b/fastchat/serve/gradio_web_server.py index 8941c6ecb..061a75a93 100644 --- a/fastchat/serve/gradio_web_server.py +++ b/fastchat/serve/gradio_web_server.py @@ -927,6 +927,7 @@ def build_single_model_ui(models, add_promotion_links=False): label="Scroll down and start chatting", height=650, show_copy_button=True, + sanitize_html=True, latex_delimiters=[ {"left": "$", "right": "$", "display": False}, {"left": "$$", "right": "$$", "display": True}, From 30207272fbe5cb5aab5e76a12323fdf402be384b Mon Sep 17 00:00:00 2001 From: xianren Date: Sat, 21 Mar 2026 17:14:03 +0800 Subject: [PATCH 2/2] style: fix formatting Co-Authored-By: Claude Opus 4.6 (1M context) --- fastchat/conversation.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fastchat/conversation.py b/fastchat/conversation.py index ac30eba01..6c44c7fe7 100644 --- a/fastchat/conversation.py +++ b/fastchat/conversation.py @@ -374,9 +374,7 @@ def to_gradio_chatbot(self): img_str = f'user upload image' elif image.image_format == ImageFormat.BYTES: img_str = f'user upload image' - msg = img_str + html.escape( - msg.replace("\n", "").strip() - ) + msg = img_str + html.escape(msg.replace("\n", "").strip()) else: if msg is not None: msg = html.escape(msg)