diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-09 15:48:53 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-09 15:48:53 +0200 |
| commit | 56776934c2ddfbad4884b252da6f5fb25864b8db (patch) | |
| tree | b34aa2a9b71a3129c937d9d0cecaa39505afbd6b /packages/meshbay-hub/tests/test_security_headers.py | |
| parent | 78b309d18efdd227c0efa42464988eaaf1483c16 (diff) | |
| download | meshbay-56776934c2ddfbad4884b252da6f5fb25864b8db.tar.gz | |
fix: the PDF preview needs object-src and frame-src, in both policies
A PDF preview showed the "this browser will not display the PDF inline"
fallback everywhere — in the desktop client since its first launch, and in
the browser since the hub started sending a CSP on 2026-09-01. It read as a
missing native feature because before that commit the hub sent no policy at
all, so Chrome had once worked and the application never had.
Two directives govern one feature. `files-app.js` decrypts the file in the
page and hands it to `<object type="application/pdf">` from a Blob; Chromium
loads that as plugin data (`object-src`, absent and therefore falling back to
`default-src 'none'`) and then renders it in an internal frame (`frame-src`).
Opening either alone changes nothing visible — the second refusal produces the
same fallback. `'self'` covers neither: a same-origin `blob:` URL is not
matched by it in either directive, measured in Chrome 152 against the deployed
page and in Electron 44 against the client's own policy.
`plugins` stays at its default `false`: the built-in viewer is not behind that
flag on Electron 44, verified by rendering one.
Widening `object-src` from `'none'` to `blob:` admits only what page script
minted itself, at a type this code sets — PDFium parsing bytes that came from
a node, which is what any browser does with the same file once downloaded.
Tests: each policy is pinned to carry `blob:` in both directives (each fails
if either token is removed), and the two policies are now held identical
directive by directive apart from the two deliberate differences — the comment
claiming they were the same had already drifted and nothing checked it. The
CSP source parser in test_desktop_shell.py read `//` comment lines as
directives, which is the "parse directives, not text" mistake this file
already records; it skips them now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XauykfBvRrpy6RYbF6F7Wu
Diffstat (limited to 'packages/meshbay-hub/tests/test_security_headers.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_security_headers.py | 40 |
1 files changed, 36 insertions, 4 deletions
diff --git a/packages/meshbay-hub/tests/test_security_headers.py b/packages/meshbay-hub/tests/test_security_headers.py index 44dd7e8..b068656 100644 --- a/packages/meshbay-hub/tests/test_security_headers.py +++ b/packages/meshbay-hub/tests/test_security_headers.py @@ -46,7 +46,9 @@ async def test_even_a_404_carries_the_headers(client): def test_the_policy_is_locked_down_where_it_matters(): - assert "default-src 'none'" in CSP # covers object-src, etc. + # 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 @@ -95,6 +97,38 @@ def test_the_streamed_download_frame_is_allowed(): 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. @@ -122,9 +156,7 @@ def test_the_two_framing_headers_agree(): Checked as a pair rather than one value apiece, because the defect was the disagreement and either one alone reads as correct. """ - import asyncio - - from meshbay_hub.app import create_app # noqa: F401 (import check) + 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] |