diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/webapp.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/webapp.py | 32 |
1 files changed, 28 insertions, 4 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py index 96b93bb..0d9d1f8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py @@ -93,9 +93,13 @@ _NO_STORE = {"Cache-Control": "no-store"} # Content-Security-Policy for the whole hub, applied by a middleware in app.py. # -# This is the *same* policy the desktop client's protocol handler already sends -# for these exact UI files (`meshbay-client/src/main.js`), plus the two reCAPTCHA -# hosts the sign-up widget loads its script, challenge iframe and images from. +# This is the desktop client's policy for these exact UI files +# (`meshbay-client/src/main.js`), which serves the same interface, and the two +# have to be changed together — a directive added here and not there breaks the +# app, and the reverse leaves the browser behind. They differ in two places, on +# purpose: `frame-ancestors` is `'self'` here and `'none'` there (nothing frames +# an `app://` page), and `frame-src` carries `'self'` here for the streamed +# download's hidden iframe, which the client has no service worker for. # `'unsafe-inline'` is style-only — htm/preact set inline `style=` attributes # everywhere; nothing inline executes, and the shell below carries no inline # `<script>`. `'wasm-unsafe-eval'` is required for the Argon2id WASM. The hub's @@ -111,6 +115,26 @@ CSP = "; ".join([ "font-src 'self'", "connect-src 'self' https: wss:", "worker-src 'self'", + # `object-src` exists for one thing, and `frame-src`'s `blob:` for the same + # thing: previewing a PDF without writing it anywhere. + # + # `files-app.js` decrypts the file in the page, wraps it in a Blob and hands + # it to `<object type="application/pdf">`. Chromium renders that with its own + # viewer, which needs TWO permissions — the resource is loaded as plugin data + # (`object-src`) and then rendered in an internal frame (`frame-src`). This + # policy had neither: `object-src` was absent, so it fell back to + # `default-src 'none'`, and the fallback message ("this browser will not + # display the PDF inline") was shown on every platform from the day this CSP + # shipped. It read as a desktop-client limitation because the client has sent + # a CSP since its first launch and the hub had none before this file. + # + # `'self'` does not cover a same-origin `blob:` URL in either directive — + # measured, not assumed, in Chrome 152 against the deployed page — so the + # token is `blob:` and it is needed in both. Widening `object-src` from + # `'none'` 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 it is downloaded. + "object-src blob:", # `'self'` is not decoration: the streamed-download path works by navigating # a hidden iframe to `/_mbdl/<id>` so the service worker is asked for the # response it is holding. Without it Chrome refuses the frame, the worker is @@ -118,7 +142,7 @@ CSP = "; ".join([ # happen — on Firefox and Safari that is the *only* way to write a large # file to disk, so the whole path was dead. Added when reCAPTCHA needed a # frame, which is why nobody connected the two. - f"frame-src 'self' {_RECAPTCHA_SRC}", + f"frame-src 'self' blob: {_RECAPTCHA_SRC}", # `'self'`, not `'none'`, and the difference is one same-origin iframe. # # The threat frame-ancestors answers is clickjacking: a *foreign* page |