Repository navigation
fix(producer,engine): harden render paths, render slots and local listeners - #7508
Conversation
A request whose options fail the strict render-request validator now gets a 400 on both the blocking and streaming routes, and its inline project directory is removed. Validate workers up front like the other overrides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed directory Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment id Map a media element id to one plain path segment before it names a frame directory or an intermediate audio file. Ids that already are one plain segment map to themselves, so existing paths stay the same; the producer and the distributed plan derive the same segment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The file server feeds the co-located capture browser, which loads it from localhost. A caller that wants another interface passes hostname. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nder server A web page on another origin can no longer start a render or lint through the server's POST routes. Requests without an Origin header (CLI tools, server-to-server callers) and same-origin pages are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r host Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jrusso1020
left a comment
There was a problem hiding this comment.
Reviewed at 38b41067f794a0753a6a028da0ac9b938b532614.
Important: generated media path segments need a disjoint, bounded namespace. packages/engine/src/utils/mediaPathSegment.ts:8-11 maps a/b to a_b~c14cddc033f6, but a distinct authored ID literally equal to a_b~c14cddc033f6 passes through unchanged. Both now use the same audio scratch files and frame directories. For example, audioMixer's ${mediaIdPathSegment(element.id)}-trimmed.wav writes the second clip over the first. This is deterministic and does not require finding a hash collision. The prior two paths were distinct.
The same generated-name branch keeps the entire readable ID: three 100-character components separated by / become one 315-byte filename. On this filesystem the prior nested directories can be created, while the new path throws ENAMETOOLONG. This adds a render failure for an otherwise representable ID.
Fix the helper with a reserved generated prefix (escape/hash any otherwise-safe ID that uses that prefix too), and bound the readable portion by filename bytes before appending the digest. Alternatively enforce unique mapped names before any writes, but retaining distinct names is preferable. Keep all existing writer/reader call sites on this single helper. Add tests for the generated-name-as-another-ID pair, long multi-component IDs and multibyte readable names.
Evidence on Node v22.22.3 through pytest-gate.sh: all 15 added mediaPathSegment tests pass, but two controlled probes using the actual transpiled helper fail. The first printed mappedA: a_b~c14cddc033f6, mappedB: a_b~c14cddc033f6, samePath: true, firstNow: second audio after real temporary-file writes. The second printed baseline nested id: success; mapped segment byte length: 315, then ENAMETOOLONG. These are helper/consumer-path probes, not a full render run. Independent adversarial source review confirmed the naming is otherwise propagated through extraction, deferred fill, compiled staging and distributed readers.
The job-construction cleanup, workers validation and compiler containment look sound. Pre-existing follow-up: the browser guard compares the Origin host with Host; it does not solve DNS rebinding or distinguish schemes. That does not block the added refusal of ordinary cross-origin requests.
Need: yes, confine render paths and release rejected jobs · Size: ok · Reuse: existing isPathInside and shared render preparation · Simpler: fix the single naming helper · Structure: fine (checked shared media writers/readers and render handlers; no split)
Risk: medium - path aliases can replace one clip's scratch output with another.
Verdict: REQUEST_CHANGES.
Reasoning: make generated media names distinct and filesystem-safe before using them across all render paths.
Reviewed by hf-review.
…server posts A same-origin page behind a reverse proxy that rewrites the Host header sent an Origin that no longer matched Host, so the cross-origin guard refused it. The guard now uses Sec-Fetch-Site when the browser sends it and falls back to the Origin and Host comparison only for browsers that do not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Verdict: REQUEST CHANGES · 0 new blockers · 2 important (open from @jrusso1020) · 1 nit · head f4339e9
+1 with @jrusso1020. Both of their mediaIdPathSegment findings are still open at this head: mediaPathSegment.ts hasn't changed since 38b4106, and the only later commit touches server.ts and its test.
- important
packages/engine/src/utils/mediaPathSegment.ts:7-11: a generated name for an unsafe id can equal a plain id. Idsa/banda_b~c14cddc033f6both map toa_b~c14cddc033f6, so the second clip's-trimmed.wavand frame dir overwrite the first (checked with a probe of the helper). Fix: reserve the generated form, so a plain id that matches it also takes the hash branch. Add a test for this pair. - important same lines: the readable part has no byte cap.
x*100/y*100/z*100produces a 315-byte segment, and视频×30/视频×30produces 374 bytes. Both throwENAMETOOLONGwhere the old nested path worked. Fix: truncate by UTF-8 bytes before the digest. - nit: the body has no
fails without the fix:line naming tests. I checked by putting back the baseserver.ts,render/shared.tsand enginefileServer.ts: the new cross-origin, workers, slot, sub-composition and loopback tests go red on base and green on head.
Scope
I read the full diff. Cross-origin guard, render-slot ordering (both acquire sites), sub-composition containment and the engine 127.0.0.1 bind all hold. Every media-id path site goes through the helper. Ran engine vitest 49/49, producer vitest 313/313 and producer bun planV2 68/68. CI is green.
— Genesis
… within filename limits A plain media id that already ends like a generated name now takes the hashed branch too, so it can no longer share frame folders or audio work files with the unsafe id that produced that name. The readable part of a generated name is capped at 64 UTF-8 bytes before the digest, so long or multibyte ids that contain separators stay within the 255-byte filename limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, jrusso1020 and Genesis. Replies to each finding:
|
There was a problem hiding this comment.
Verdict: APPROVE — 2 resolved, 0 open, 0 new · f4339e9 → 110d3d1. A plain id that ends in ~<12 hex> now goes through the hash branch, so plain and generated names can no longer meet. The readable part is capped at 64 UTF-8 bytes on code-point boundaries. Probe: a/b, a_b~c14cddc033f6, 3×100-char, CJK, emoji, .., . and empty ids all give distinct segments of 77 bytes or less, and mkdir works for each. The 4 new tests fail on the previous helper; 19/19 pass at head. All CI is green, Windows included.
— Genesis
jrusso1020
left a comment
There was a problem hiding this comment.
Reviewed at 110d3d18d4cbe55ef9a50107488d2e2e7093d3e7. Delta since my previous review, including the follow-up browser-header change, read.
Both prior naming concerns are resolved in packages/engine/src/utils/mediaPathSegment.ts: generated-form IDs enter the same mapping branch, and the readable generated portion is capped by UTF-8 bytes on character boundaries. Writer/reader call sites continue to share this helper. The server predicate handles browser site metadata and retains the Origin/Host fallback.
Validation through pytest-gate.sh, Node 22.22.3: mediaPathSegment.test.ts 19/19 pass, original filesystem probes now preserve separate outputs and create the formerly overlong generated directory (77 bytes), and 9 actual server-predicate cases pass (same-origin/proxy accepted; cross-site, same-site and unknown metadata refused; absent-metadata Origin/Host and malformed-Origin cases checked). The native cross-origin route suite could not load because this checkout's reused dependencies lack hono; the predicate probe is not an HTTP integration test. No full render or mixed-version worker run.
Overlap check: all 143 open PRs and their paginated file lists inspected. Shared-file patches in #7083, #6612, #5820, #5815, #5698, #5334 and #4599 address different behavior; none duplicates or changes the naming helper or browser predicate. Same-file work will still need normal integration/rebase care.
Pre-existing follow-up: plain IDs still retain their original filenames, including platform and length constraints. The mixed-version naming limitation remains documented in the PR.
Need: yes, bounded and consistent render paths · Size: ok · Reuse: existing shared media naming helper and render app predicate · Simpler: already minimal · Structure: fine (checked extraction, staging and distributed readers; no split)
Risk: medium - shared scratch paths and standalone request handling; full render integration not exercised locally.
Verdict: APPROVE
Reasoning: Both previous blockers are fixed and their original filesystem probes now pass. No new blocker found in the delta.
Reviewed by hf-review.
What
Hardening for the producer and engine render paths:
workersis validated up front (0, fractional or negative values now return 400 instead of 500).data-composition-srccarries./,\or:get a generated name: a readable part capped at 64 UTF-8 bytes, then~and a 12-character digest of the whole id. A plain id that already ends like a generated name (~plus 12 hex characters) also gets a generated name, so a plain id can never share a folder or work file with an unsafe one. Generated names stay within the 255-byte filename limit for long or multibyte ids.127.0.0.1by default, matching the producer file server, with an opt-inhostnameoption.Sec-Fetch-Siteheader decides when it is present, so a same-origin page behind a reverse proxy that rewritesHoststill works. Browsers without that header fall back to comparingOriginwithHost. Requests without anOriginheader (curl, server-to-server) are unchanged, and the server never sent CORS headers, so no working cross-origin client exists. Embedders that mountcreateRenderHandlerson their own app are not affected.Why
Each change keeps untrusted request or composition input from reaching paths, slots or listeners it should not. Valid compositions render the same.
Related work
The
<script src>inlining guard is handled separately in #4599.How
Small, local changes in
server.ts,render/shared.ts, the media path helpers and the engine file server. One existingplanV2.test.tscase expected an id like../escapeto be rejected; that id is now kept inside the frame folder instead, so the case was updated.Test plan
mediaIdPathSegment > never gives a plain id the name generated for another id,on disk > keeps the audio work files of a colliding pair apart,on disk > names a frame folder and work file for a long multi-component id,on disk > names a frame folder and work file for a long multibyte id, shown by runningmediaPathSegment.test.tsagainst the previous helper (4 failed, 15 passed). A reviewer separately confirmed that the cross-origin, workers, render-slot, sub-composition (writes sub-compositions only inside compileDir) and loopback (answers on loopback and not on another interface) tests go red with the baseserver.ts,render/shared.tsand enginefileServer.tsrestored./,\or:now get a different frame folder name. A distributed plan written by this version and read by an older chunk worker (or the reverse) would not find the frames for such ids. Plain ids keep their paths, so this only matters during a mixed-version rollout.maintoo (local ffmpeg 8.0.1); this change adds no failures.🤖 Generated with Claude Code