Browse Source

Security hardening

maziggy 1 day ago
parent
commit
ecddbf2b92

+ 3 - 1
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.

+ 48 - 0
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 = "<script>alert(1)</script>"
+
+
+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 "&lt;script&gt;alert(1)&lt;/script&gt;" 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 "<strong>Good part</strong>" in _render_confirm_prompt_page(_request(), archive, "good")
+    assert "<strong>Rejected</strong>" in _render_confirm_prompt_page(_request(), archive, "reject")