diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index d66bb7063f..dad184c54b 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -1,4 +1,3 @@ -import re import uuid from datetime import datetime from typing import Any, Literal, TypeAlias @@ -68,48 +67,6 @@ class AppListQuery(BaseModel): raise ValueError("Invalid UUID format in tag_ids.") from exc -# XSS prevention: patterns that could lead to XSS attacks -# Includes: script tags, iframe tags, javascript: protocol, SVG with onload, etc. -_XSS_PATTERNS = [ - r"]*>.*?", # Script tags - r"]*?(?:/>|>.*?)", # Iframe tags (including self-closing) - r"javascript:", # JavaScript protocol - r"]*?\s+onload\s*=[^>]*>", # SVG with onload handler (attribute-aware, flexible whitespace) - r"<.*?on\s*\w+\s*=", # Event handlers like onclick, onerror, etc. - r"]*(?:\s*/>|>.*?)", # Object tags (opening tag) - r"]*>", # Embed tags (self-closing) - r"]*>", # Link tags with javascript -] - - -def _validate_xss_safe(value: str | None, field_name: str = "Field") -> str | None: - """ - Validate that a string value doesn't contain potential XSS payloads. - - Args: - value: The string value to validate - field_name: Name of the field for error messages - - Returns: - The original value if safe - - Raises: - ValueError: If the value contains XSS patterns - """ - if value is None: - return None - - value_lower = value.lower() - for pattern in _XSS_PATTERNS: - if re.search(pattern, value_lower, re.DOTALL | re.IGNORECASE): - raise ValueError( - f"{field_name} contains invalid characters or patterns. " - "HTML tags, JavaScript, and other potentially dangerous content are not allowed." - ) - - return value - - class CreateAppPayload(BaseModel): name: str = Field(..., min_length=1, description="App name") description: str | None = Field(default=None, description="App description (max 400 chars)", max_length=400) @@ -118,11 +75,6 @@ class CreateAppPayload(BaseModel): icon: str | None = Field(default=None, description="Icon") icon_background: str | None = Field(default=None, description="Icon background color") - @field_validator("name", "description", mode="before") - @classmethod - def validate_xss_safe(cls, value: str | None, info) -> str | None: - return _validate_xss_safe(value, info.field_name) - class UpdateAppPayload(BaseModel): name: str = Field(..., min_length=1, description="App name") @@ -133,11 +85,6 @@ class UpdateAppPayload(BaseModel): use_icon_as_answer_icon: bool | None = Field(default=None, description="Use icon as answer icon") max_active_requests: int | None = Field(default=None, description="Maximum active requests") - @field_validator("name", "description", mode="before") - @classmethod - def validate_xss_safe(cls, value: str | None, info) -> str | None: - return _validate_xss_safe(value, info.field_name) - class CopyAppPayload(BaseModel): name: str | None = Field(default=None, description="Name for the copied app") @@ -146,11 +93,6 @@ class CopyAppPayload(BaseModel): icon: str | None = Field(default=None, description="Icon") icon_background: str | None = Field(default=None, description="Icon background color") - @field_validator("name", "description", mode="before") - @classmethod - def validate_xss_safe(cls, value: str | None, info) -> str | None: - return _validate_xss_safe(value, info.field_name) - class AppExportQuery(BaseModel): include_secret: bool = Field(default=False, description="Include secrets in export") diff --git a/api/tests/unit_tests/controllers/console/app/test_xss_prevention.py b/api/tests/unit_tests/controllers/console/app/test_xss_prevention.py deleted file mode 100644 index 313818547b..0000000000 --- a/api/tests/unit_tests/controllers/console/app/test_xss_prevention.py +++ /dev/null @@ -1,254 +0,0 @@ -""" -Unit tests for XSS prevention in App payloads. - -This test module validates that HTML tags, JavaScript, and other potentially -dangerous content are rejected in App names and descriptions. -""" - -import pytest - -from controllers.console.app.app import CopyAppPayload, CreateAppPayload, UpdateAppPayload - - -class TestXSSPreventionUnit: - """Unit tests for XSS prevention in App payloads.""" - - def test_create_app_valid_names(self): - """Test CreateAppPayload with valid app names.""" - # Normal app names should be valid - valid_names = [ - "My App", - "Test App 123", - "App with - dash", - "App with _ underscore", - "App with + plus", - "App with () parentheses", - "App with [] brackets", - "App with {} braces", - "App with ! exclamation", - "App with @ at", - "App with # hash", - "App with $ dollar", - "App with % percent", - "App with ^ caret", - "App with & ampersand", - "App with * asterisk", - "Unicode: 测试应用", - "Emoji: 🤖", - "Mixed: Test 测试 123", - ] - - for name in valid_names: - payload = CreateAppPayload( - name=name, - mode="chat", - ) - assert payload.name == name - - def test_create_app_xss_script_tags(self): - """Test CreateAppPayload rejects script tags.""" - xss_payloads = [ - "", - "", - "", - "", - "", - "", - ] - - for name in xss_payloads: - with pytest.raises(ValueError) as exc_info: - CreateAppPayload(name=name, mode="chat") - assert "invalid characters or patterns" in str(exc_info.value).lower() - - def test_create_app_xss_iframe_tags(self): - """Test CreateAppPayload rejects iframe tags.""" - xss_payloads = [ - "", - "", - "", - ] - - for name in xss_payloads: - with pytest.raises(ValueError) as exc_info: - CreateAppPayload(name=name, mode="chat") - assert "invalid characters or patterns" in str(exc_info.value).lower() - - def test_create_app_xss_javascript_protocol(self): - """Test CreateAppPayload rejects javascript: protocol.""" - xss_payloads = [ - "javascript:alert(1)", - "JAVASCRIPT:alert(1)", - "JavaScript:alert(document.cookie)", - "javascript:void(0)", - "javascript://comment%0Aalert(1)", - ] - - for name in xss_payloads: - with pytest.raises(ValueError) as exc_info: - CreateAppPayload(name=name, mode="chat") - assert "invalid characters or patterns" in str(exc_info.value).lower() - - def test_create_app_xss_svg_onload(self): - """Test CreateAppPayload rejects SVG with onload.""" - xss_payloads = [ - "", - "", - "", - ] - - for name in xss_payloads: - with pytest.raises(ValueError) as exc_info: - CreateAppPayload(name=name, mode="chat") - assert "invalid characters or patterns" in str(exc_info.value).lower() - - def test_create_app_xss_event_handlers(self): - """Test CreateAppPayload rejects HTML event handlers.""" - xss_payloads = [ - "
", - "", - "", - "", - "", - "
", - "", - ] - - for name in xss_payloads: - with pytest.raises(ValueError) as exc_info: - CreateAppPayload(name=name, mode="chat") - assert "invalid characters or patterns" in str(exc_info.value).lower() - - def test_create_app_xss_object_embed(self): - """Test CreateAppPayload rejects object and embed tags.""" - xss_payloads = [ - "", - "", - "", - ] - - for name in xss_payloads: - with pytest.raises(ValueError) as exc_info: - CreateAppPayload(name=name, mode="chat") - assert "invalid characters or patterns" in str(exc_info.value).lower() - - def test_create_app_xss_link_javascript(self): - """Test CreateAppPayload rejects link tags with javascript.""" - xss_payloads = [ - "", - "", - ] - - for name in xss_payloads: - with pytest.raises(ValueError) as exc_info: - CreateAppPayload(name=name, mode="chat") - assert "invalid characters or patterns" in str(exc_info.value).lower() - - def test_create_app_xss_in_description(self): - """Test CreateAppPayload rejects XSS in description.""" - xss_descriptions = [ - "", - "javascript:alert(1)", - "", - ] - - for description in xss_descriptions: - with pytest.raises(ValueError) as exc_info: - CreateAppPayload( - name="Valid Name", - mode="chat", - description=description, - ) - assert "invalid characters or patterns" in str(exc_info.value).lower() - - def test_create_app_valid_descriptions(self): - """Test CreateAppPayload with valid descriptions.""" - valid_descriptions = [ - "A simple description", - "Description with < and > symbols", - "Description with & ampersand", - "Description with 'quotes' and \"double quotes\"", - "Description with / slashes", - "Description with \\ backslashes", - "Description with ; semicolons", - "Unicode: 这是一个描述", - "Emoji: 🎉🚀", - ] - - for description in valid_descriptions: - payload = CreateAppPayload( - name="Valid App Name", - mode="chat", - description=description, - ) - assert payload.description == description - - def test_create_app_none_description(self): - """Test CreateAppPayload with None description.""" - payload = CreateAppPayload( - name="Valid App Name", - mode="chat", - description=None, - ) - assert payload.description is None - - def test_update_app_xss_prevention(self): - """Test UpdateAppPayload also prevents XSS.""" - xss_names = [ - "", - "javascript:alert(1)", - "", - ] - - for name in xss_names: - with pytest.raises(ValueError) as exc_info: - UpdateAppPayload(name=name) - assert "invalid characters or patterns" in str(exc_info.value).lower() - - def test_update_app_valid_names(self): - """Test UpdateAppPayload with valid names.""" - payload = UpdateAppPayload(name="Valid Updated Name") - assert payload.name == "Valid Updated Name" - - def test_copy_app_xss_prevention(self): - """Test CopyAppPayload also prevents XSS.""" - xss_names = [ - "", - "javascript:alert(1)", - "", - ] - - for name in xss_names: - with pytest.raises(ValueError) as exc_info: - CopyAppPayload(name=name) - assert "invalid characters or patterns" in str(exc_info.value).lower() - - def test_copy_app_valid_names(self): - """Test CopyAppPayload with valid names.""" - payload = CopyAppPayload(name="Valid Copy Name") - assert payload.name == "Valid Copy Name" - - def test_copy_app_none_name(self): - """Test CopyAppPayload with None name (should be allowed).""" - payload = CopyAppPayload(name=None) - assert payload.name is None - - def test_edge_case_angle_brackets_content(self): - """Test that angle brackets with actual content are rejected.""" - # Angle brackets without valid HTML-like patterns should be checked - # The regex pattern <.*?on\w+\s*= should catch event handlers - # But let's verify other patterns too - - # Valid: angle brackets used as symbols (not matched by our patterns) - # Our patterns specifically look for dangerous constructs - - # Invalid: actual HTML tags with event handlers - invalid_names = [ - "
", - "", - ] - - for name in invalid_names: - with pytest.raises(ValueError) as exc_info: - CreateAppPayload(name=name, mode="chat") - assert "invalid characters or patterns" in str(exc_info.value).lower()