test_q4b_ui_security.py python
103 lines 3.1 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago
1 """Security tests for Track Q / Q4b Path B UI redesign (§Q4A.15)."""
2
3 from __future__ import annotations
4
5 import re
6 import urllib.error
7 import urllib.request
8 from pathlib import Path
9
10 import pytest
11
12 from tests.fixtures.app import seed_app_repo, start_test_app
13 from tools.app.server import STATIC_ROOT
14
15 FORBIDDEN_ROUTES = [
16 "/api/init",
17 "/api/sync",
18 "/api/verify-step",
19 "/api/route",
20 "/api/merge",
21 ]
22
23 FORBIDDEN_COPY = [
24 "Sign up",
25 "Create account",
26 "Run your agents here",
27 "Website executes tasks",
28 "Install unsigned desktop build as primary path",
29 "Requires MuseHub",
30 ]
31
32 HTTPS_CTA_RE = re.compile(r'href="(https://[^"]+)"')
33
34
35 def test_no_new_endpoints_or_auth_disable(tmp_path: Path) -> None:
36 seed_app_repo(tmp_path)
37 handle, client = start_test_app(tmp_path)
38 try:
39 status, payload = client.request("GET", "/api/status", headers={})
40 assert status == 401
41 assert payload["error"] == "auth"
42
43 for path in FORBIDDEN_ROUTES:
44 code, body = client.get(path)
45 assert code == 404
46 assert body["error"] == "not_found"
47
48 bad_csrf, csrf_payload = client.request(
49 "POST",
50 "/api/governance-sync",
51 body={"write": False},
52 headers={
53 "Authorization": f"Bearer {client.session}",
54 "X-Overseer-CSRF": "wrong",
55 "Content-Type": "application/json",
56 },
57 )
58 assert bad_csrf == 403
59 assert csrf_payload["error"] == "csrf"
60 finally:
61 handle.shutdown()
62
63
64 def test_diagram_path_escape_refused(tmp_path: Path) -> None:
65 seed_app_repo(tmp_path)
66 handle, client = start_test_app(tmp_path)
67 try:
68 for path in (
69 "/assets/diagrams/../../server.py",
70 "/assets/diagrams/../../../LICENSE",
71 "/assets/../../cli/main.py",
72 ):
73 req = urllib.request.Request(
74 f"{client.base_url}{path}",
75 headers={"Authorization": f"Bearer {client.session}"},
76 )
77 with pytest.raises(urllib.error.HTTPError) as exc_info:
78 urllib.request.urlopen(req, timeout=5)
79 assert exc_info.value.code in {403, 404}
80 finally:
81 handle.shutdown()
82
83
84 def test_external_ctas_https_only_and_no_remote_script() -> None:
85 html = (STATIC_ROOT / "index.html").read_text(encoding="utf-8")
86 js = (STATIC_ROOT / "assets" / "app.js").read_text(encoding="utf-8")
87 css = (STATIC_ROOT / "assets" / "app.css").read_text(encoding="utf-8")
88
89 for href in HTTPS_CTA_RE.findall(html):
90 assert href.startswith("https://")
91 assert not href.startswith("http://")
92
93 assert 'src="http' not in html
94 assert "unpkg.com" not in html + js + css
95 assert "jsdelivr" not in html + js + css
96 assert "cdn.jsdelivr" not in html + js + css
97 assert "mermaid.min.js" not in html.lower()
98 assert "mermaid@" not in html.lower()
99 assert '<script src="https://' not in html
100
101 for phrase in FORBIDDEN_COPY:
102 assert phrase not in html
103 assert phrase not in js
File History 1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago