Repository navigation
fix(studio-server): refuse cross-origin writes and unknown hosts on the preview server - #7490
Conversation
Non-GET routes in createStudioApi answer 403 unless the shared sameOriginPost predicate (lifted into studio-server's ./request-identity subpath) accepts the request: the Host must be a server identity, a present Sec-Fetch-Site must be same-origin, and a present Origin must match the effective host and scheme. Hosts behind Codespaces or tunnels allow their public name with HYPERFRAMES_PREVIEW_ALLOWED_HOSTS, read by `hyperframes preview` and the Studio dev server. Squashed from #6128 (42574a7, 110a300, 89011e1, 41c6626, 27193bb) onto current main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every route of `hyperframes preview` (the forwarded studio API, the SSE stream, /__hyperframes_config and the Studio page) now answers only loopback names, the HYPERFRAMES_PREVIEW_HOST bind, or a host listed in HYPERFRAMES_PREVIEW_ALLOWED_HOSTS, the same list the write guard uses. Only the Host header counts here, since a page can set X-Forwarded-Host on its own requests and a read carries no Origin to compare it with. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
Edit accuracy: accurate 267 (base branch 267), smooth 103 of thoseThe gate passes. Quarantined, measured but not gated (8)
Unstable (1)
|
jerrai-bot-heygen
left a comment
There was a problem hiding this comment.
Exact-head security review at 2028188: no demonstrated bypass in the reviewed local-server routes. The Studio API checks state-changing requests against allowed Host/effective origin and Sec-Fetch-Site; a missing Origin is intentionally permitted for non-browser clients, so this is browser CSRF/DNS-rebinding containment, not authentication. The CLI preview adds an outer actual-Host check before the page, configuration, API forwarding and SSE, ignoring spoofed X-Forwarded-Host for reads. Allowed extra hostnames are exact (case-insensitive) and ports are handled; I found no separate WebSocket upgrade handler in this listener. Hosted origin-guard/CLI tests executed, but the capture-evidence check and a Studio browser test were red at review; neither failure was established as caused by this guard, and not all CI was green.
Cut Room upgrade compatibility: EF's EditorManager advertises a public_host, launches hyperframes preview bound to 0.0.0.0, and currently passes only HYPERFRAMES_PREVIEW_HOST, not HYPERFRAMES_PREVIEW_ALLOWED_HOSTS. When that launcher upgrades to this CLI, a non-local public alias absent from the allowlist gets 403 for even the Studio page while its loopback health probe stays green. The launcher needs to pass the public hostname into the new allowlist (or hold its CLI pin). I sent cutroom-dev this conditional upgrade advisory; it is not a claim of a current hosted outage. No local preview/browser run, approval, release or merge by me. — Jerrai
There was a problem hiding this comment.
Verdict: APPROVE · 0 blocker · 1 important · 0 nits · head 2028188
The CLI changes hold up. refuseUnknownHost reads only Host. A rebinding page that sets X-Forwarded-Host on a write is still refused, because its Origin names its own host, not the forwarded one. CI passed on the second attempt: the first Test (studio) failure was Chrome failing to start twice on the runner, and the PR doesn't cause it. One gap remains in the Vite dev server (inline).
Scope
Read in full: requestIdentity.ts, createStudioApi.ts guard, studioServer.ts middleware and /api forwarder, desktopRoutes.ts, telemetryIdentity.ts move, vite.config.ts, packaging/subpath. Checked the new Host/allowed-host tests in studioServer.test.ts. Outside this repo: HFI desktop pins studio-server 0.9.28 and has its own loopback Host/Origin guard, which this guard agrees with (loopback Host, http:// origin). heygen-verse imports @hyperframes/core/studio-api, not this package. I did not run the tests locally.
— Genesis
| // Same env var as the CLI preview server: extra hosts whose | ||
| // writes the API accepts (Codespaces, tunnels). | ||
| _api = mod.createStudioApi(adapter, { | ||
| allowedHosts: mod.previewAllowedHostsFromEnv(), |
There was a problem hiding this comment.
important — In the dev server, HYPERFRAMES_PREVIEW_ALLOWED_HOSTS only affects API writes, and API reads skip the Host check. This plugin calls server.middlewares.use directly inside configureServer. Vite 6 runs those hooks before it adds hostCheckMiddleware (_createServer: hooks loop, then middlewares.use(hostCheckMiddleware(...))), so /api/* runs before Vite's Host check.
Two results:
- With
pnpm devon a tunnel host and the variable set, writes pass, but Vite still blocks the Studio page becauseserver.allowedHostsdoesn't include the host. The 403 message tells the user to set a variable that can't fix it here. - A rebinding page can read
/api/projectsand project files from a running dev server. This existed before the PR, but the PR closes the same hole in the CLI.
Suggested fix: compute the list once in the config, pass it to server.allowedHosts, and start this API middleware with if (!hostHeaderAllowed(req.headers.host, allowedHosts)) { res.writeHead(403); return res.end(); }. That makes the dev server match hyperframes preview. A follow-up PR is fine, since this only affects contributors.
|
checked the squash against #6128. same files, same originGuard tests, the only deltas are the read guard and its tests. thanks for carrying it over. |
…tream heygen-com#7490) The studio API now rejects state-changing requests whose Origin is another site (including the opaque null origin) and answers only the hosts the server is reached by, closing the DNS-rebinding and cross-site write path into a project served on loopback. The host-identity helpers move from the CLI's telemetryIdentity.ts into the new @hyperframes/studio-server request-identity subpath, which the CLI re-exports. Ported unchanged from upstream f318d3d (James Russo); it applied with a three-way merge. The new originGuard tests failed before the change (12 failed) and pass after it (21 passed); cli src/server 20 files pass (246 passed, 10 skipped) once the studio-server dist is rebuilt. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nce watermark Seventeen commits, PRs 7469-7493, issues 7473 and 7486 and twelve pin moves are recorded with their evidence in the continued ledger, DECISIONS, UPSTREAM and ROADMAP. Adopted: heygen-com#7466, heygen-com#7479, heygen-com#7482, heygen-com#7490 and the heygen-com#7441 and heygen-com#7440 docs. Deferred: heygen-com#7469, heygen-com#7471, heygen-com#7476, heygen-com#7480, heygen-com#7468, heygen-com#7487, heygen-com#7491. Not applicable: heygen-com#7470, heygen-com#7351, heygen-com#7475, heygen-com#7483. The watermark moves to 61e720b, PR heygen-com#7493, issue heygen-com#7486; thirteen new PRs are pinned. FORK.md records the subComposition test differences. The core packed-size roadmap row trigger is not met. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
What
The local studio server now refuses requests that don't come from the Studio it serves. Writes from another site get a 403, and every route of
hyperframes previewanswers only the host names the server is reached by. This carries @benjaminkincses's #6128 onto current main as one commit under his authorship, plus one commit on top that extends the host check to reads.Why
The studio API has no authentication; binding to loopback is its only protection. Browsers send simple POSTs (
text/plain, form data) cross-origin without a preflight, so a page open in the same browser could create files and start renders whilehyperframes previewruns. A page served under a name that resolves to 127.0.0.1 could also read API responses, because nothing checked the Host header outside three routes.Related work
Supersedes #6128. Its branch couldn't be updated from here: the main merge brings LFS objects that a maintainer can't upload to a fork, and the repo's rules refuse its unsigned and merge commits. His commits are squashed into
0453cc5with him as the author. Thank you, Benjamin.How
createStudioApianswers 403 to a non-GET request unless the sharedsameOriginPostpredicate accepts it, from the new@hyperframes/studio-server/request-identitysubpath. The predicate requires a known Host, asame-originSec-Fetch-Site when one is sent, and an Origin that matches the effective host and scheme. Hosts behind Codespaces or a tunnel allow their public name withHYPERFRAMES_PREVIEW_ALLOWED_HOSTS, whichhyperframes previewand the Studio dev server read. A refused write names the host and the variable./api/events,/__hyperframes_configand the Studio page. It answers only loopback names, theHYPERFRAMES_PREVIEW_HOSTbind, or an allowed host, and gives the same JSON 403 otherwise. Only theHostheader counts here. A page can setX-Forwarded-Hoston its own requests, and a read carries no Origin to compare it with. The check sits on the CLI server rather than increateStudioApi, so apps that embed the API on their own domain keep their read behavior.No visible change
Studio looks and behaves the same when opened from
localhost,127.0.0.1, the configured bind, or an allowed host. The only Studio change is 13 lines inpackages/studio/vite.config.ts, which pass the allowed-host list to the dev server's API. A request from any other host gets a JSON 403 instead of the page.Test plan
createStudioApi.originGuard.test.ts: 21 tests from fix(studio-server): reject cross-origin state-changing requests #6128.studioServer.test.ts: 4 new tests, which drive the realcreateStudioServer./__hyperframes_config,/api/projects, a project file,/api/eventsand/, and the body has no project path or file content.X-Forwarded-Host: localhostdoesn't rescue a foreign Host.localhost,127.0.0.1and[::1]are answered.HYPERFRAMES_PREVIEW_ALLOWED_HOSTSis answered.src/server/: 253/253. studio-server: 1673 passed, 2 skipped.hyperframes previewon a scratch project withHYPERFRAMES_PREVIEW_ALLOWED_HOSTS=demo.tunnels.example, then sent requests with curl.text/plainwriteOrigin: nullform POST to renderX-Forwarded-Host: localhostX-Forwarded-Proto)The Studio UI loaded in headless Chrome from
localhostwith no 4xx responses, and the composition rendered in the preview iframe.docs/packages/studio-server.mdx(from fix(studio-server): reject cross-origin state-changing requests #6128).Not exercised: a real Codespaces or tunnel session (only simulated with headers), and Firefox or Safari.
— Rames Jusso
🤖 Generated with Claude Code