From a15c3912008b7dc444c7fc6a2b4ccf782c647215 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 8 Sep 2026 22:53:35 +0200 Subject: fix(hub): let this origin frame its own download URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/` 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 Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST --- packages/meshbay-hub/src/meshbay_hub/app.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/app.py') diff --git a/packages/meshbay-hub/src/meshbay_hub/app.py b/packages/meshbay-hub/src/meshbay_hub/app.py index 2daa55b..7b187df 100644 --- a/packages/meshbay-hub/src/meshbay_hub/app.py +++ b/packages/meshbay-hub/src/meshbay_hub/app.py @@ -154,7 +154,22 @@ def create_app(cfg: HubConfig | None = None) -> FastAPI: response.headers.setdefault("Content-Security-Policy", CSP) response.headers.setdefault("X-Content-Type-Options", "nosniff") response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin") - response.headers.setdefault("X-Frame-Options", "DENY") + # SAMEORIGIN, matching `frame-ancestors 'self'` in the CSP above. + # + # The two say the same thing to different generations of browser, and + # they were saying different things: CSP allowed this origin to frame + # itself, this header forbade all framing. The spec says a browser must + # ignore X-Frame-Options when the CSP carries frame-ancestors — but + # relying on that while shipping a header that contradicts our own + # policy is asking to be surprised, and we were: the streamed download + # (a hidden iframe onto `/_mbdl/`, the only way to write a large + # file to disk on Firefox and Safari) stayed blocked after the CSP was + # fixed, and this header was why it looked like the fix had not worked. + # + # No foreign origin may frame this page under either spelling. That is + # the property; DENY was one notch stricter than the property needed and + # broke a feature to get there. + response.headers.setdefault("X-Frame-Options", "SAMEORIGIN") return response # Routers (webapp last — catches / before API routes) -- cgit v1.2.3