Repository navigation
fix(studio-server): reject cross-origin state-changing requests - #6128
Dextheking1 wants to merge 2 commits into
Conversation
The studio API is unauthenticated and loopback binding is the only mitigation (F-001). CORS-safelisted POSTs (text/plain, form data) are sent without a preflight, so any website open in the user's browser could create and upload project files and trigger renders while hyperframes preview runs. Add a guard in createStudioApi: non-GET requests are refused with 403 when the Origin host does not match the request Host, when the Origin is unparseable (including the opaque literal null), or when Fetch Metadata marks the request cross-site. Requests with no Origin header (curl, tests, in-process callers) and all reads are unaffected.
miguel-heygen
left a comment
There was a problem hiding this comment.
Thanks for picking this up, the threat is real and the tests go through the actual API object, which is the right way in. One gap: the guard only asks whether the Origin host equals the Host header, so a page whose DNS points its own name at 127.0.0.1 sends a matching pair and gets through. The server already has the missing half in isLoopbackHost and identityAllowed, and sameOriginPost in the CLI server applies both for the open in desktop route, including a stricter Sec-Fetch-Site rule. Could this reuse that helper, lifted somewhere both can import, so there is one answer to "is this write same origin" instead of two that disagree?
Also worth knowing: no test or lint workflow has run on this PR yet, only the mergeability check.
Replace the guard's own Origin-host-equals-Host comparison with the CLI server's sameOriginPost, lifted into studio-server as helpers/requestIdentity (new ./request-identity subpath) together with isLoopbackHost and identityAllowed. The CLI imports the same definition from there; desktopRoutes and telemetryIdentity re-export so their callers and tests do not move. The old comparison let a page whose DNS name resolves to 127.0.0.1 through, because its Origin and Host agree with each other. sameOriginPost also requires the Host to be an identity the server answers to, and refuses any Sec-Fetch-Site other than same-origin. Regression tests cover the rebinding pair, a no-Origin write on such a host, and the same-site case.
|
fixed. writeOriginAllowed is gone, the guard now calls sameOriginPost. lifted it with isLoopbackHost and identityAllowed into studio-server (helpers/requestIdentity.ts, new ./request-identity subpath in the descriptor), cli imports it from there. desktopRoutes and telemetryIdentity re-export, so their callers and tests did not move. one plumbing note: when a request has no Host header it never came over the wire, so the middleware falls back to the host in the request URL. on a real server that URL is built from the Host header, so nothing changes there. new tests in createStudioApi.originGuard.test.ts: origin and host both rebind.example (a name pointing at loopback) gets a 403 and writes nothing, same with no origin on that host, and sec-fetch-site same-site no longer passes. guard suite 11/11. local runs, ci has not run on this pr:
|
What
Non-GET routes in
createStudioApinow reject cross-origin requests. The guard compares the Origin host against the request Host and also rejects requests whose Fetch Metadata (Sec-Fetch-Site) marks them cross-site.Why
The studio API is unauthenticated, and loopback binding is the only mitigation (the F-001 note in the CLI server code). CORS-safelisted POSTs (text/plain bodies, form data) are sent without a preflight, so any website open in the user's browser could create and upload project files and trigger renders while
hyperframes previewruns.Related work
Refs the F-001 note in
packages/cli/src/server/portUtils.ts. No separate issue; self-contained fix.How
Middleware in
createStudioApi: a non-GET request gets a 403 when the Origin host does not match the request Host, when the Origin is unparseable (including the opaque literalnull), or when Sec-Fetch-Site is cross-site. Requests with no Origin header (curl, tests, in-process callers) and all reads are unaffected.Test plan
createStudioApi.originGuard.test.ts, all passing. The file is pinned to the node vitest environment on purpose: the package default (happy-dom) follows the browser fetch spec and silently drops forbidden headers such as Origin, which made the first version of the tests simulate nothing.