diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-08 22:53:35 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-08 22:53:35 +0200 |
| commit | a15c3912008b7dc444c7fc6a2b4ccf782c647215 (patch) | |
| tree | 01aa10720b9650d1a071a943a9a5be2cbf41d453 /packages/meshbay-hub/src/meshbay_hub/api/webapp.py | |
| parent | 4b94468d24913c3071b48eeefb43367f4f5cd523 (diff) | |
| download | meshbay-a15c3912008b7dc444c7fc6a2b4ccf782c647215.tar.gz | |
fix(hub): let this origin frame its own download URL
Three headers govern whether a page may be framed, and all three had to be
wrong for the streamed download to work — so fixing them one at a time cost an
afternoon of redeploys and retests. They were visible together in a single
`curl -I` against the deployed hub, which is where this should have started.
The streamed-download path navigates a hidden iframe to `/_mbdl/<id>` so the
service worker is asked for the response it is holding. On Firefox and Safari
that is the only way to write a large file to disk: neither has the File System
Access API, and OPFS is capped at 10% of the volume's size (measured on Firefox
154: 389,233,459 bytes of a 3,892,334,592-byte volume, refused to the byte),
which a film exceeds.
- `frame-src` was reCAPTCHA's two origins with no `'self'`, so the frame
could not be loaded at all. Added when the captcha needed a frame; nobody
connected the two.
- `frame-ancestors 'none'` forbids all framing, this origin included.
- `X-Frame-Options: DENY` says the same in an older dialect. The spec says a
browser must ignore it when frame-ancestors is present — relying on that
while shipping a header that contradicts our own policy is asking to be
surprised, and we were: the CSP was fixed and the download stayed broken.
`'self'` and `SAMEORIGIN` refuse every foreign origin exactly as `'none'` and
`DENY` do. The clickjacking property is untouched; what they additionally allow
is this origin framing itself, which is the only thing the download needed.
Pinned three ways: `frame-src` must carry `'self'`, `frame-ancestors` must be
`'none'` or `'self'` and never name an origin, and the two framing headers must
agree — the defect was the disagreement, and either one read as correct alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/webapp.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/webapp.py | 26 |
1 files changed, 24 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py index 3cfb208..96b93bb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py @@ -111,8 +111,30 @@ CSP = "; ".join([ "font-src 'self'", "connect-src 'self' https: wss:", "worker-src 'self'", - f"frame-src {_RECAPTCHA_SRC}", - "frame-ancestors 'none'", + # `'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 + # never asked, and the page waits out its timeout for a download that cannot + # 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}", + # `'self'`, not `'none'`, and the difference is one same-origin iframe. + # + # The threat frame-ancestors answers is clickjacking: a *foreign* page + # framing this one and stealing clicks. `'self'` refuses every foreign + # origin exactly as `'none'` does — what it additionally allows is this + # origin framing itself, which is precisely how a streamed download works + # (a hidden iframe navigates to `/_mbdl/<id>` so the service worker is + # asked for the response it holds). + # + # Under `'none'` Firefox blocked that frame, the worker was never asked, + # and every large download waited out two 15-second timeouts and then fell + # through — on Firefox and Safari that is the only way to write a large + # file to disk. Chrome did not show it: its worker intercepts the + # navigation before the network response and its CSP are ever considered, + # which is why this looked like a Firefox-only problem for an afternoon. + "frame-ancestors 'self'", "base-uri 'none'", "form-action 'none'", ]) |