fix(xss): dati query in x-data passano da tojson_attr, non interpolazione JS (#10)

select_recipe.html e recipe_list.html interpolavano recipe/lot/serial/search
dentro un letterale JS con apici singoli: un valore con `'` chiudeva la
stringa e iniettava chiavi nell'oggetto x-data (CSP con unsafe-eval).
Anche recipe_list.html:222 (toLocaleDateString) aveva lo stesso schema su
current_language. Fix: |tojson_attr ovunque.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 22:29:05 +02:00
parent 920ab2fb73
commit 51f3fe32d4
4 changed files with 102 additions and 5 deletions
@@ -27,6 +27,40 @@ class TestRecipeList:
assert resp.status_code == 302
assert "/auth/login" in resp.headers["Location"]
def test_recipe_list_search_does_not_break_out_of_x_data(
self, logged_in_client, mock_api_client
):
"""#10: ``?search=`` is JSON-encoded inside x-data, not string-interpolated.
The payload must land as the *content* of a single quoted JSON
string token (recovered by undoing only the ``\\u0027`` escape),
and the object key right after it must still be the untouched
``filter: 'all'`` from the template - proof no new key was injected.
"""
import re
from html.parser import HTMLParser
mock_api_client.get.return_value = {"items": [], "total": 0, "pages": 1}
payload = "',x:alert(1),y:'"
resp = logged_in_client.get("/maker/recipes", query_string={"search": payload})
assert resp.status_code == 200
html = resp.get_data(as_text=True)
captured = {}
class _P(HTMLParser):
def handle_starttag(self, tag, attrs):
d = dict(attrs)
if d.get("x-data") and "search:" in d["x-data"]:
captured["x_data"] = d["x-data"]
_P().feed(html)
assert "x_data" in captured
m = re.search(r'search:\s*"([^"]*)"\s*,\s*filter:\s*\'all\'', captured["x_data"])
assert m, f"search is not a single quoted JSON token: {captured['x_data']!r}"
assert m.group(1).replace("\\u0027", "'") == payload
class TestRecipeNew:
"""GET /maker/recipes/new tests."""