diff --git a/backend/app/api/routes/archives.py b/backend/app/api/routes/archives.py index d943d6d5e..0fb041dce 100644 --- a/backend/app/api/routes/archives.py +++ b/backend/app/api/routes/archives.py @@ -3518,7 +3518,9 @@ def _render_confirm_prompt_page(request: Request, archive: PrintArchive, verdict with JavaScript off -- gets the same page and presses the button. """ name = html_escape(archive.print_name or archive.filename or "") - label = _VERDICT_LABELS.get(verdict, verdict) + # Escaped although the route only lets good/reject through: the page must + # not depend on a check made in another function. + label = html_escape(_VERDICT_LABELS.get(verdict, verdict)) # No action attribute: the form posts back to the URL the page was loaded # from, so it works behind a reverse proxy and on a host external_url does # not name. diff --git a/backend/tests/unit/test_confirm_page_escaping.py b/backend/tests/unit/test_confirm_page_escaping.py new file mode 100644 index 000000000..db51ea777 --- /dev/null +++ b/backend/tests/unit/test_confirm_page_escaping.py @@ -0,0 +1,48 @@ +"""The one-tap outcome prompt page escapes what it echoes (#1898). + +The routes only let ``good`` / ``reject`` through as the verdict, but the page +renderer must not rely on that: it is served unauthenticated, and a check made +in another function can be loosened without anyone looking at this one. +""" + +from __future__ import annotations + +from types import SimpleNamespace + +from starlette.requests import Request + +from backend.app.api.routes.archives import _render_confirm_prompt_page + +PAYLOAD = "" + + +def _request() -> Request: + return Request( + { + "type": "http", + "method": "GET", + "path": "/api/v1/archives/confirm/tok/x", + "query_string": b"", + "headers": [], + } + ) + + +def test_an_unknown_verdict_is_escaped(): + archive = SimpleNamespace(print_name="Benchy", filename="benchy.3mf") + page = _render_confirm_prompt_page(_request(), archive, PAYLOAD) + assert PAYLOAD not in page + assert "<script>alert(1)</script>" in page + + +def test_the_print_name_is_escaped(): + archive = SimpleNamespace(print_name=PAYLOAD, filename="x.3mf") + page = _render_confirm_prompt_page(_request(), archive, "good") + assert PAYLOAD not in page + assert "Good part" in page + + +def test_known_verdicts_keep_their_labels(): + archive = SimpleNamespace(print_name="Benchy", filename="benchy.3mf") + assert "Good part" in _render_confirm_prompt_page(_request(), archive, "good") + assert "Rejected" in _render_confirm_prompt_page(_request(), archive, "reject")