aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/webapp.py26
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/app.py17
2 files changed, 40 insertions, 3 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'",
])
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/<id>`, 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)