Repository navigation
fix(dashboard): accept read batches behind an HTTPS proxy - #2250
Open
bmdavis419 wants to merge 1 commit into
Open
bmdavis419 wants to merge 1 commit into
bmdavis419 wants to merge 1 commit into
Conversation
The dashboard sends reads that start together as one POST /api/dashboard/batch, and the batch route admitted it only when the page's Origin equalled the URL rebuilt from Host and X-Forwarded-Proto. Behind a proxy that ends TLS, such as the self-host image behind Caddy, Cloudflare Tunnel or Railway, that URL is http:// while the page is https://, so every batch got an empty 403 and the dashboard showed "Could not reach the server" on whichever panels it batched. The browser's Sec-Fetch-Site now decides when it is present: only same-origin passes, and any other value is refused even when Origin matches. Without the header, the exact Origin check still applies. Fixes UsefulSoftwareCo#2199 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Behind a proxy that ends TLS, such as Caddy, Cloudflare Tunnel, Tailscale Serve or Railway's edge in front of the self-host image, the dashboard fails reads at random. An app's overview shows "Unable to complete this request", the header shows "Could not reach the server", and a reload brings the page back until the next client-side navigation.
The dashboard sends reads that start together as one
POST /api/dashboard/batch.sameOrigininpackages/dashboard-start/src/implementation/batch-host.tsadmitted a batch only when the page'sOriginequalledHttpServerRequest.toURL(request).origin, which Effect rebuilds fromHostandX-Forwarded-Proto. The image's Go host reaches workerd over plain HTTP, and itsReverseProxy.Rewritedrops the outer proxy'sX-Forwarded-Proto, so that URL is alwayshttp://<host>while the page ishttps://<host>. Every batch got an empty 403, and every read in it failed.The browser already knows whether the page and the URL it sends the batch to share an origin, and reports it in
Sec-Fetch-Site, which no page can set.sameOriginnow lets that header decide when it is present:same-originpasses, and any other value (same-site,cross-site,none) is refused even whenOriginmatches. Without the header (plain-HTTP hosts other than loopback, browsers without Fetch Metadata), the exactOrigincheck applies as before. This is the rule of Go'snet/http.CrossOriginProtection, slightly stricter:noneis refused, a request with neitherSec-Fetch-SitenorOriginis refused, and the fallback compares the whole origin rather than the host. Executor already readsSec-Fetch-Siteas a veto (telemetry.ts,mcp-approvals.ts, mcp-auth'soauth.ts); this is the first placesame-originadmits a request.Thanks to zackleman for #2246, which fixed this in the Go host and showed every batch failing on Railway. That route has the host claim
X-Forwarded-Proto: httpswheneverBETTER_AUTH_URLis https, which also refuses batches for anyone who opens such a server over plain HTTP (http://localhost:4400, a LAN address), changes the URLtoURLbuilds for every request (the server-rendered document request, the in-process origin), and has the host set a forwarding header from configuration. Comparing with the configured origin, asrequireUserLivedoes, would instead refuse batches on every other hostname of the same instance (a tailnet name beside the public one), although the same reads sent one at a time are accepted there. This change touches only the batch check:toURL, the Go host andlayerWithBatchesare unchanged, and Cloud's batches from browsers that sendSec-Fetch-Siteno longer depend on Cloudflare'sX-Forwarded-Proto.Fixes #2199
Evidence
dashboardReadBatchesnow sends the batch a browser behind a TLS proxy sends (Origin: https://<host>,Sec-Fetch-Site: same-origin) and expects 200 with the member's own viewer read, then 403 forsame-site,cross-siteandnonewith the target's own origin.expected 403 to be 200atproxied.status.same-originor a matchingOrigin):same-site: expected 200 to be 403. The loop pins the strict rule.dashboardBuildChange,dashboardRequestVolume,serverRenderedDashboard,serverRenderedAccess,queryRefresh,retainedReturn,membersRefresh.dashboardRequestVolume,serverRenderedDashboard,queryRefresh,retainedReturn,membersRefresh,browserConnectionFailures,billingPolling.bun run checkpasses.tls internal, then a throwaway container with fresh data andBETTER_AUTH_URL=https://executor.test:9443.2.0.0-beta.12: every dashboard batch got 403. The browser sentOrigin: https://executor.test:9443andSec-Fetch-Site: same-origin. After a client-side navigation, the app overview and Connections showed "Unable to complete this request". A second hostname for the same container also got 403.X-Forwarded-Proto: https, which the host drops. Results were identical forH= theBETTER_AUTH_URLhost and for a second hostname with a port (box.tailnet.test:10006):Origin: https://H,Sec-Fetch-Site: same-origin(a browser behind TLS)Origin: https://H, noSec-Fetch-SiteOrigin: http://H, noSec-Fetch-Site(plain HTTP)Origin: http://H,Sec-Fetch-Site: cross-siteOrigin: https://H,Sec-Fetch-Site: same-siteOrigin: https://attacker.example,Sec-Fetch-Site: cross-siteOrigin: https://attacker.example, noSec-Fetch-SiteMerge Danger
Door: two-way. Reverting
sameOriginrestores the old rule.Blast radius: dashboard read batches on self-host and Cloud. Only the check on
POST /api/dashboard/batchchanges.Sec-Fetch-Siteis present and notsame-originis now refused even when itsOriginmatches (row d). The dashboard batches only same-origin reads, so no browser flow sends one.Sec-Fetch-Site, or a front proxy that stripsSec-Fetch-*, still gets 403, as before.🤖 Generated with Claude Code