From 2f912ff619449afb69eee380e2083500ecda84bb Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 21 Jul 2026 18:43:48 +0200 Subject: [PATCH] fix(docs): constrain user runtime links --- src/govoplan_docs/backend/api/v1/routes.py | 5 ++++- tests/test_docs_context.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/govoplan_docs/backend/api/v1/routes.py b/src/govoplan_docs/backend/api/v1/routes.py index c59d9c9..fa72532 100644 --- a/src/govoplan_docs/backend/api/v1/routes.py +++ b/src/govoplan_docs/backend/api/v1/routes.py @@ -681,7 +681,10 @@ def _bounded_string_list(value: object, *, maximum_items: int, maximum_length: i def _user_link_allowed(link: DocumentationLink) -> bool: href = link.href.strip() if link.kind == "runtime": - return href.startswith("/") and not href.startswith("//") + if not href.startswith("/") or href.startswith("//") or "\\" in href: + return False + parsed = urlsplit(href) + return not parsed.scheme and not parsed.netloc if link.kind != "public": return False parsed = urlsplit(href) diff --git a/tests/test_docs_context.py b/tests/test_docs_context.py index 1528955..18c2a83 100644 --- a/tests/test_docs_context.py +++ b/tests/test_docs_context.py @@ -102,6 +102,7 @@ class DocsContextTests(unittest.TestCase): links=( DocumentationLink(label="Open task", href="/example", kind="runtime"), DocumentationLink(label="Unsafe runtime", href="javascript:alert(1)", kind="runtime"), + DocumentationLink(label="Backslash runtime", href="/\\evil.invalid", kind="runtime"), DocumentationLink(label="Public help", href="https://example.invalid/help", kind="public"), DocumentationLink(label="Repository", href="example/docs/SECRET.md", kind="repository"), DocumentationLink(label="API", href="/api/v1/example", kind="api"),