Skip to content

Commit 3e30663

Browse files
authored
fix: prevent XSS via javascript: URIs in recipe actions (#6885)
1 parent a72641b commit 3e30663

File tree

2 files changed

+47
-2
lines changed

2 files changed

+47
-2
lines changed

mealie/schema/household/group_recipe_action.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from enum import Enum
22
from typing import Any
33

4-
from pydantic import UUID4, ConfigDict
4+
from pydantic import UUID4, ConfigDict, field_validator
55

66
from mealie.schema._mealie import MealieModel
77
from mealie.schema.response.pagination import PaginationBase
@@ -22,6 +22,14 @@ class CreateGroupRecipeAction(MealieModel):
2222

2323
model_config = ConfigDict(use_enum_values=True)
2424

25+
@field_validator("url")
26+
def validate_url_scheme(url: str) -> str:
27+
"""Validate that the URL uses a safe scheme to prevent XSS via javascript: URIs."""
28+
url_lower = url.lower().strip()
29+
if not (url_lower.startswith("http://") or url_lower.startswith("https://")):
30+
raise ValueError("URL must use http or https scheme")
31+
return url
32+
2533

2634
class SaveGroupRecipeAction(CreateGroupRecipeAction):
2735
group_id: UUID4

tests/integration_tests/user_household_tests/test_group_recipe_actions.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def create_action(action_type: GroupRecipeActionType = GroupRecipeActionType.lin
2525
return CreateGroupRecipeAction(
2626
action_type=action_type,
2727
title=random_string(),
28-
url=random_string(),
28+
url=f"https://example.com/{random_string()}",
2929
)
3030

3131

@@ -194,3 +194,40 @@ def test_group_recipe_actions_trigger_invalid_type(api_client: TestClient, uniqu
194194
)
195195

196196
assert response.status_code == 400
197+
198+
199+
@pytest.mark.parametrize(
200+
"url,should_pass",
201+
[
202+
("https://example.com", True),
203+
("http://example.com", True),
204+
("HTTPS://EXAMPLE.COM", True),
205+
("HTTP://EXAMPLE.COM", True),
206+
("javascript:alert('xss')", False),
207+
("JAVASCRIPT:alert('xss')", False),
208+
("data:text/html,<script>alert('xss')</script>", False),
209+
("file:///etc/passwd", False),
210+
("ftp://example.com", False),
211+
("//example.com", False),
212+
("example.com", False),
213+
],
214+
)
215+
def test_group_recipe_actions_url_scheme_validation(
216+
api_client: TestClient, unique_user: TestUser, url: str, should_pass: bool
217+
):
218+
"""Test that only http and https URLs are allowed to prevent XSS via javascript: URIs."""
219+
action_data = {
220+
"action_type": "link",
221+
"title": random_string(),
222+
"url": url,
223+
}
224+
response = api_client.post(
225+
api_routes.households_recipe_actions,
226+
json=action_data,
227+
headers=unique_user.token,
228+
)
229+
230+
if should_pass:
231+
assert response.status_code == 201
232+
else:
233+
assert response.status_code == 422

0 commit comments

Comments
 (0)