""" 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/` 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 ``. 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")