mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-01-23 17:22:56 -05:00
fix: prevent XSS via javascript: URIs in recipe actions (#6885)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any
|
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._mealie import MealieModel
|
||||||
from mealie.schema.response.pagination import PaginationBase
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
@@ -22,6 +22,14 @@ class CreateGroupRecipeAction(MealieModel):
|
|||||||
|
|
||||||
model_config = ConfigDict(use_enum_values=True)
|
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):
|
class SaveGroupRecipeAction(CreateGroupRecipeAction):
|
||||||
group_id: UUID4
|
group_id: UUID4
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def create_action(action_type: GroupRecipeActionType = GroupRecipeActionType.lin
|
|||||||
return CreateGroupRecipeAction(
|
return CreateGroupRecipeAction(
|
||||||
action_type=action_type,
|
action_type=action_type,
|
||||||
title=random_string(),
|
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
|
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,<script>alert('xss')</script>", 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
|
||||||
|
|||||||
Reference in New Issue
Block a user