1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
|
"""
The hub sends a Content-Security-Policy and the other protective headers on
every response — the SPA shell, its assets, and the API alike.
Second-review L5 / third-review M5: previously there were none, so an injection
that landed in the SPA (rendered third-party OG data, a federated group name,
chat content) had nothing stopping it from loading more code or exfiltrating.
"""
import pytest
from meshbay_hub.api.webapp import CSP
def _directive(csp: str, name: str) -> str:
for part in csp.split(";"):
part = part.strip()
if part == name or part.startswith(name + " "):
return part
return ""
@pytest.mark.asyncio
async def test_the_spa_shell_carries_the_policy(client):
r = await client.get("/")
assert r.headers["content-security-policy"] == CSP
assert r.headers["x-content-type-options"] == "nosniff"
assert r.headers["x-frame-options"] == "SAMEORIGIN"
assert "referrer-policy" in r.headers
@pytest.mark.asyncio
async def test_the_api_carries_the_headers_too(client):
r = await client.get("/v1/health")
assert r.status_code == 200
assert "content-security-policy" in r.headers
assert r.headers["x-content-type-options"] == "nosniff"
@pytest.mark.asyncio
async def test_even_a_404_carries_the_headers(client):
# The middleware runs on every response, so a probe for a missing path
# cannot be framed or content-sniffed either.
r = await client.get("/no/such/path")
assert r.status_code == 404
assert r.headers["x-frame-options"] == "SAMEORIGIN"
def test_the_policy_is_locked_down_where_it_matters():
# The catch-all. It no longer "covers object-src" — that directive is set
# explicitly below, and this comment used to say otherwise.
assert "default-src 'none'" in CSP
# `'self'`, not `'none'`: every foreign origin is still refused, which is
# the whole of the clickjacking protection. What `'self'` adds is this
# origin framing itself, which the streamed download needs — see
# test_the_streamed_download_frame_is_allowed. Under `'none'` Firefox
# blocked it and large downloads there had no path to disk at all.
assert _directive(CSP, "frame-ancestors") == "frame-ancestors 'self'"
assert _directive(CSP, "base-uri") == "base-uri 'none'"
script = _directive(CSP, "script-src")
# The hub's own origin must not be able to serve executable script (T3):
# 'self' and the wasm token are fine, a bare `https:` scheme is not.
assert "'self'" in script and "'wasm-unsafe-eval'" in script
assert "https:" not in script.split()
def test_recaptcha_is_the_only_external_origin():
hosts = {"https://www.google.com", "https://www.gstatic.com"}
for part in CSP.split(";"):
for tok in part.strip().split()[1:]:
if tok.startswith(("http://", "https://")):
assert tok in hosts, f"unexpected external origin in CSP: {tok}"
def test_the_streamed_download_frame_is_allowed():
"""
`frame-src` must carry `'self'`, and this is not a preference.
The streamed-download path works by navigating a hidden iframe to
`/_mbdl/<id>` so the service worker is asked for the response it is already
holding. `frame-src` was tightened to reCAPTCHA's two origins when the
captcha needed a frame, and nobody connected the two: Chrome refused the
frame, the worker was never asked, and the page waited out its timeout for
a download that could not happen. On Firefox and Safari that is the *only*
way to write a large file to disk — there is no File System Access API and
OPFS is capped at 10% of the volume — so the whole path was dead, silently,
on the deployed hub.
Found by clicking Download three times and watching nothing happen, with
the reason in the browser console and nowhere else.
"""
frame_src = _directive(CSP, "frame-src")
assert "'self'" in frame_src, (
"the same-origin download frame is blocked; large downloads fall back "
"to memory, or are refused outright above the ceiling")
# And still no wildcard: `'self'` is what the download needs, nothing more.
assert "*" not in frame_src
def test_the_pdf_preview_has_both_permissions_it_needs():
"""
Two directives govern one feature, and fixing either alone changes nothing
visible.
`files-app.js` decrypts a PDF in the page and shows it from a Blob through
`<object type="application/pdf">`. Chromium's viewer loads that as plugin
data (`object-src`) and renders it in an internal frame (`frame-src`).
`object-src` was absent, so it fell back to `default-src 'none'` and every
preview showed the "will not display the PDF inline" fallback instead —
measured in Chrome 152 against the deployed page, where the violation was
`object-src` and the fallback was on screen. It had been broken here since
this file started sending a policy (2026-09-01); before that the hub sent
none, which is why the browser was believed to be the working one.
`'self'` is not what either directive needs: a same-origin `blob:` URL is
not matched by `'self'`, so with `object-src` opened the preview still
failed at `frame-src`. Both carry `blob:`, and neither carries a wildcard.
"""
for name in ("object-src", "frame-src"):
directive = _directive(CSP, name)
assert directive, f"no {name} directive"
sources = directive.split()[1:]
assert "blob:" in sources, (
f"{name} refuses the decrypted PDF; the preview shows its fallback "
f"message on every browser")
assert "*" not in sources
# A blob URL is minted by this page's own script. Nothing else needs
# these directives, so nothing else belongs in them.
assert "data:" not in sources
def test_no_foreign_origin_may_frame_this_page():
"""The clickjacking property, stated separately from how it is spelled.
`frame-ancestors` moved from `'none'` to `'self'` so the streamed download
could frame its own URL. That must not become a list of origins, and it must
never become `*`: the threat is a foreign page framing this one and stealing
clicks, and `'self'` is the most permissive value that still refuses every
one of them.
"""
value = _directive(CSP, "frame-ancestors").split(" ", 1)[1].strip()
assert value in ("'none'", "'self'"), (
f"frame-ancestors is {value!r}: anything naming an origin lets that "
f"origin frame this page")
def test_the_two_framing_headers_agree():
"""X-Frame-Options and CSP must say the same thing.
They did not: the CSP let this origin frame itself (which the streamed
download needs) while `X-Frame-Options: DENY` forbade all framing. The spec
says a browser must ignore the header when frame-ancestors is present, and
counting on that while shipping a contradiction is how an afternoon goes:
the CSP was fixed, the download stayed broken, and the header was why.
Checked as a pair rather than one value apiece, because the defect was the
disagreement and either one alone reads as correct.
"""
from meshbay_hub.app import create_app # noqa: F401 (import check)
ancestors = _directive(CSP, "frame-ancestors").split(" ", 1)[1].strip()
expected = {"'none'": "DENY", "'self'": "SAMEORIGIN"}[ancestors]
assert expected == "SAMEORIGIN", (
"if frame-ancestors goes back to 'none', X-Frame-Options must go back "
"to DENY in app.py — and the streamed download will stop working again")
|