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:
@@ -32,6 +32,69 @@ class TestSelectRecipe:
|
||||
assert resp.status_code == 302
|
||||
assert "/auth/login" in resp.headers["Location"]
|
||||
|
||||
def test_select_recipe_query_params_do_not_break_out_of_x_data(
|
||||
self, logged_in_client, mock_api_client, monkeypatch
|
||||
):
|
||||
"""#10: recipe/lot query params are JSON-encoded inside x-data.
|
||||
|
||||
A raw ``'{{ auto_lot }}'`` interpolation lets a value containing a
|
||||
single quote close the JS string literal and inject new keys into
|
||||
the Alpine ``x-data`` object (reflected XSS, CSP has unsafe-eval).
|
||||
The fix is ``|tojson_attr``: the payload must survive only as the
|
||||
*content* of a single quoted JSON string token, never as JS syntax
|
||||
that opens a new object key (``lot_number:`` must directly follow).
|
||||
"""
|
||||
import re
|
||||
from html.parser import HTMLParser
|
||||
|
||||
monkeypatch.setenv("STATION_CODE", "ST-TEST")
|
||||
import config
|
||||
import importlib
|
||||
importlib.reload(config)
|
||||
import blueprints.measure
|
||||
importlib.reload(blueprints.measure)
|
||||
|
||||
mock_api_client.get_station_recipes.return_value = []
|
||||
|
||||
payload = "',x:alert(document.domain),y:'"
|
||||
resp = logged_in_client.get("/measure/select", query_string={"recipe": 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*lot_number:', 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
|
||||
|
||||
def test_select_recipe_lot_with_apostrophe_stays_valid_json_string(
|
||||
self, logged_in_client, mock_api_client, monkeypatch
|
||||
):
|
||||
"""#10: a lot number like ``L'23`` must not break the page."""
|
||||
monkeypatch.setenv("STATION_CODE", "ST-TEST")
|
||||
import config
|
||||
import importlib
|
||||
importlib.reload(config)
|
||||
import blueprints.measure
|
||||
importlib.reload(blueprints.measure)
|
||||
|
||||
mock_api_client.get_station_recipes.return_value = []
|
||||
|
||||
resp = logged_in_client.get("/measure/select", query_string={"lot": "L'23"})
|
||||
assert resp.status_code == 200
|
||||
# Raw HTML (pre-entity-decoding): the value is a JSON string whose
|
||||
# delimiting quotes are the HTML entity, and the apostrophe is the
|
||||
# JS unicode escape - never a bare ``'`` that could end the token.
|
||||
assert "lot_number: "L\\u002723"" in resp.get_data(as_text=True)
|
||||
|
||||
|
||||
class TestTaskList:
|
||||
"""GET /measure/tasks/<recipe_id> tests."""
|
||||
|
||||
Reference in New Issue
Block a user