Skip to content

fix(studio-server): refuse cross-origin writes and unknown hosts on the preview server - #7490

Merged
jrusso1020 merged 2 commits into
mainfrom
fix/studio-api-same-host-writes
Nov 10, 2026
Merged

jrusso1020 merged 2 commits into
mainfrom
fix/studio-api-same-host-writes

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

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 preview answers 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 while hyperframes preview runs. 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 0453cc5 with him as the author. Thank you, Benjamin.

How

  • Writes (fix(studio-server): reject cross-origin state-changing requests #6128, unchanged): createStudioApi answers 403 to a non-GET request unless the shared sameOriginPost predicate accepts it, from the new @hyperframes/studio-server/request-identity subpath. The predicate requires a known Host, a same-origin Sec-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 with HYPERFRAMES_PREVIEW_ALLOWED_HOSTS, which hyperframes preview and the Studio dev server read. A refused write names the host and the variable.
  • Reads (new): a middleware on the CLI preview server checks every route: the forwarded API, /api/events, /__hyperframes_config and the Studio page. It answers only loopback names, the HYPERFRAMES_PREVIEW_HOST bind, or an allowed host, and gives the same JSON 403 otherwise. Only the Host header counts here. A page can set X-Forwarded-Host on its own requests, and a read carries no Origin to compare it with. The check sits on the CLI server rather than in createStudioApi, 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 in packages/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

  • Unit tests added/updated
    • 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 real createStudioServer.
      • A foreign Host gets 403 on /__hyperframes_config, /api/projects, a project file, /api/events and /, and the body has no project path or file content.
      • X-Forwarded-Host: localhost doesn't rescue a foreign Host.
      • localhost, 127.0.0.1 and [::1] are answered.
      • A host in HYPERFRAMES_PREVIEW_ALLOWED_HOSTS is answered.
      • With the middleware disabled, 3 of the 4 fail.
    • CLI src/server/: 253/253. studio-server: 1673 passed, 2 skipped.
  • Manual testing performed: I built the CLI and ran hyperframes preview on a scratch project with HYPERFRAMES_PREVIEW_ALLOWED_HOSTS=demo.tunnels.example, then sent requests with curl.
Request Result
Loopback config, page, file read 200
Same-origin write 201, file created
Cross-site text/plain write 403, no file
Origin: null form POST to render 403
Foreign Host: config, project list, file read, SSE 403, no path or content in the body
Foreign Host with X-Forwarded-Host: localhost 403
Allowed tunnel host read 200
Allowed tunnel host write (https Origin, X-Forwarded-Proto) 201

The Studio UI loaded in headless Chrome from localhost with no 4xx responses, and the composition rendered in the preview iframe.

Not exercised: a real Codespaces or tunnel session (only simulated with headers), and Firefox or Safari.

— Rames Jusso

🤖 Generated with Claude Code

Dextheking1 and others added 2 commits November 10, 2026 09:34
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>
@mintlify

mintlify Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Nov 10, 2026, 9:38 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@github-actions

github-actions Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 103 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

Unstable (1)

  • seqab-none-center-r0-root-z100: tracking 0.03, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.03, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.03, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. With pnpm dev on a tunnel host and the variable set, writes pass, but Vite still blocks the Studio page because server.allowedHosts doesn't include the host. The 403 message tells the user to set a variable that can't fix it here.
  2. A rebinding page can read /api/projects and 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.

@benjaminkincses

Copy link
Copy Markdown

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.

@jrusso1020
jrusso1020 added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit f318d3d Nov 10, 2026
280 of 312 checks passed
@jrusso1020
jrusso1020 deleted the fix/studio-api-same-host-writes branch November 10, 2026 10:33
@miguel-heygen miguel-heygen mentioned this pull request Nov 10, 2026
3 of 4 tasks
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Nov 10, 2026
…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>
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Nov 10, 2026
…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>

This branch was successfully deployed

1 active deployment
staging - docs — 2028188a Deployed Nov 10, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants