Skip to content

fix(studio-server): buildSubCompositionHtml reads only compositions inside the project - #7482

Merged
jrusso1020 merged 3 commits into
mainfrom
fix/subcomposition-path-confinement
Nov 10, 2026
Merged

jrusso1020 merged 3 commits into
mainfrom
fix/subcomposition-path-confinement

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

What

buildSubCompositionHtml returns null for a compPath that resolves outside projectDir, the same answer it gives for a missing file. It also leaves out the project index.html head when that file resolves outside the project, for example through a symlink.

Why

The helper is exported from @hyperframes/studio-server (and re-exported by @hyperframes/core/studio-api), and it read join(projectDir, compPath) without a containment check, trusting each caller to confine the path first. Studio's own preview route does (resolveWithinProject in routes/preview.ts). An embedder that forgets would serve any file the process can read. Moving the check into the helper means no caller has to remember it.

Related work

Defense in depth for an embedder-side fix that landed separately; no public issue.

How

compFile comes from core's resolveWithinProject (@hyperframes/core/safe-path), the helper the rest of Studio uses, which also follows symlinks before the containment check. A null result returns null before any existsSync or readFileSync.

Test plan

  • New tests in subComposition.test.ts: a ../<sibling>/secret.txt path and an absolute path both return null (fails on main); an index.html symlinked to a file outside the project contributes no head content (fails at the previous head). All 27 pass here.

  • Behavior change for local Studio: a project whose index.html is a symlink to a file outside the project loses that head in sub-composition previews, matching how the preview route already treats a composition file symlinked outside.

  • vitest run src/routes/preview in studio-server: 159 pass (186 with the helper tests). That route already confines its own path, so its behavior is unchanged.

  • oxlint and oxfmt --check are clean on both files, and check:package-subpaths passes.

  • Unit tests added/updated

  • Manual testing performed

  • Documentation updated (if applicable)

  • Comments follow CONTRIBUTING.md "Comments"

— Rames Jusso

🤖 Generated with Claude Code

jrusso1020 and others added 2 commits November 10, 2026 07:37
…nside the project

The helper joined projectDir with compPath and read the result, trusting
every caller to have confined the path first. Studio's own preview route
does; an embedder that forgets would serve any readable file. The helper
now resolves compPath with core's resolveWithinProject and returns null
for a path outside the project, the same answer as a missing file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 105 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 (2)

  • seqab-none-px-r0-nested-z100: tracking 0.07, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.07, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.07, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true
  • seqplainkeys-none-px-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, 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.

Security re-review at 036c976: the new resolveWithinProject guard confines the requested composition file, including an existing outside-pointing symlink. Hosted support tests exercised the new spec (26 passing); no local test or baseline run by me.

Residual read path: packages/studio-server/src/helpers/subComposition.ts:336–339 separately reads the project's index.html via join/readFileSync, without applying resolveWithinProject. If index.html is a symlink to a file outside the project, a valid in-project composition passes the new check, yet the helper incorporates the outside file's <head> content in its returned HTML. This was present before this PR, but the stated defense-in-depth guarantee that the helper only reads within the project remains incomplete. Please confine that secondary read too, and cover an existing outside-pointing symlink with a test. I have source-verified the call chain but not run this case. Also the new sibling-path test builds a path using outside.split('/'); on Windows it may not construct the actual sibling path, so its green result does not prove the old escape was tested there.

Findings-only COMMENT, not an approval or merge. — Jerrai

… inside the project

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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.

Re-review at 28b52977: the prior secondary-read finding is addressed. buildSubCompositionHtml now resolves both the requested composition and the separate project index.html through resolveWithinProject before either file read. The new regression uses a valid in-project scene plus an outside-pointing index.html symlink, asserts the scene remains present and the external head marker is absent, and all 27 helper tests executed successfully in the exact-head hosted support job. I found no new code blocker in this incremental fix. The guard checks existing paths/symlinks but is not an atomic open against concurrent symlink replacement; no such attacker-controlled race was established here. No deployed test or local test by me. Findings-only COMMENT, not approval or merge. — 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 · 0 important · 0 nits · 28b52977

Both reads in buildSubCompositionHtml (the composition and the root index.html head) now go through resolveWithinProject, so .., absolute paths and outside-pointing symlinks return null or no head. Studio's own preview route already confines compPath the same way, so local Studio behaves the same apart from the symlinked-index.html case the body already describes. Required CI is green at this head, and the new tests ran in Test (support) (27 pass).

Scope

I read subComposition.ts, its test, core/src/safePath.ts and the only in-repo caller (routes/preview.ts). Jerrai's earlier finding about the secondary index.html read is fixed at this head. Out of scope: assetExists still probes join(projectDir, …) for asset refs. That only checks whether a file exists and doesn't return its contents, and it was there before this PR.

— Genesis

@jrusso1020
jrusso1020 added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit 724ab39 Nov 10, 2026
128 of 129 checks passed
@jrusso1020
jrusso1020 deleted the fix/subcomposition-path-confinement branch November 10, 2026 08:48
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Nov 10, 2026
…ject (upstream heygen-com#7482)

buildSubCompositionHtml resolved the composition path and the root
index.html with a bare join, so a path with .. segments or an absolute
path, or a symlinked index.html, could read a file outside the project into
the preview. Both now go through resolveWithinProject and return null (or
leave the head out) when the path leaves the project. Ported from upstream
724ab39 (James Russo) by hand because the file imports differ.

Fork changes to the upstream test: the escape path is built with
relative()/sep because the upstream split("/") never forms a traversal on
Windows (the traversal case passed before the fix there), and the symlink
case is skipped when the process cannot create symlinks (EPERM without
Developer Mode). The traversal case failed before the source change and
passes after it: subComposition.test.ts 22 passed, 1 skipped.

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>
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Nov 10, 2026
…opped in e591201

The manual port of upstream heygen-com#7482 removed the existing test that loads the
default gsap version when the project head has no gsap. Restored unchanged;
subComposition.test.ts is 23 passed, 1 skipped.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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.

2 participants