diff --git a/mealie/schema/household/group_recipe_action.py b/mealie/schema/household/group_recipe_action.py index fca10c25d..92ae2f836 100644 --- a/mealie/schema/household/group_recipe_action.py +++ b/mealie/schema/household/group_recipe_action.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Any -from pydantic import UUID4, ConfigDict +from pydantic import UUID4, ConfigDict, field_validator from mealie.schema._mealie import MealieModel from mealie.schema.response.pagination import PaginationBase @@ -22,6 +22,14 @@ class CreateGroupRecipeAction(MealieModel): model_config = ConfigDict(use_enum_values=True) + @field_validator("url") + def validate_url_scheme(url: str) -> str: + """Validate that the URL uses a safe scheme to prevent XSS via javascript: URIs.""" + url_lower = url.lower().strip() + if not (url_lower.startswith("http://") or url_lower.startswith("https://")): + raise ValueError("URL must use http or https scheme") + return url + class SaveGroupRecipeAction(CreateGroupRecipeAction): group_id: UUID4 diff --git a/tests/integration_tests/user_household_tests/test_group_recipe_actions.py b/tests/integration_tests/user_household_tests/test_group_recipe_actions.py index 941e58dd0..3a4c4ec4c 100644 --- a/tests/integration_tests/user_household_tests/test_group_recipe_actions.py +++ b/tests/integration_tests/user_household_tests/test_group_recipe_actions.py @@ -25,7 +25,7 @@ def create_action(action_type: GroupRecipeActionType = GroupRecipeActionType.lin return CreateGroupRecipeAction( action_type=action_type, title=random_string(), - url=random_string(), + url=f"https://example.com/{random_string()}", ) @@ -194,3 +194,40 @@ def test_group_recipe_actions_trigger_invalid_type(api_client: TestClient, uniqu ) assert response.status_code == 400 + + +@pytest.mark.parametrize( + "url,should_pass", + [ + ("https://example.com", True), + ("http://example.com", True), + ("HTTPS://EXAMPLE.COM", True), + ("HTTP://EXAMPLE.COM", True), + ("javascript:alert('xss')", False), + ("JAVASCRIPT:alert('xss')", False), + ("data:text/html,", False), + ("file:///etc/passwd", False), + ("ftp://example.com", False), + ("//example.com", False), + ("example.com", False), + ], +) +def test_group_recipe_actions_url_scheme_validation( + api_client: TestClient, unique_user: TestUser, url: str, should_pass: bool +): + """Test that only http and https URLs are allowed to prevent XSS via javascript: URIs.""" + action_data = { + "action_type": "link", + "title": random_string(), + "url": url, + } + response = api_client.post( + api_routes.households_recipe_actions, + json=action_data, + headers=unique_user.token, + ) + + if should_pass: + assert response.status_code == 201 + else: + assert response.status_code == 422