fix(cli,core,lint,producer,studio-server): terminate ffprobe options everywhere, pin the contract - #2945
Conversation
miguel-heygen
left a comment
There was a problem hiding this comment.
Exact-head review: the production argv edits are mechanically correct at every current ffprobe seam, #2917’s missing callers are included, targeted contract tests pass 12/12, and CI is green. I am holding the stamp for two P2s:
-
packages/producer/src/utils/ffprobeArgvContract.test.ts:20does not actually pin the claimed contract. Its regex misses removal of--from three current production seams (packages/engine/src/utils/ffprobe.ts:52,packages/cli/src/commands/init.ts:112,packages/cli/src/whisper/transcribe.ts:333). The coverage assertion only checksSCANNED.length >= 11, so a new/unlisted caller is invisible, and arbitrary"json", identifier,source can false-positive. Please discover actual ffprobe call/argv expressions, compare the discovered caller set to the manifest, and mutation-test removal/misordering at every supported shape. At minimum,audioPadTrimshould validate--is penultimate rather than merely present. -
packages/producer/src/services/render/audioPadTrim.ts:466still exposes many exact input paths through the new error path.redactTelemetryStringdoes not cover relative paths or arbitrary absolute roots. Real ffprobe stderr for-customer-secret-intro.mp4is returned unchanged, as are/data/acme-secret/video.mp4andcustomer/acme-secret/video.mp4, and that string propagates viaPadTrimAudioResult.error. Please redact the exact validated input path and basename before the generic scrub, with tests for a dash-prefixed relative path and a non-allowlisted absolute root.
The runtime -- placements themselves are clean; these findings are about making the contract durable and the newly described redaction truthful.
miguel-heygen
left a comment
There was a problem hiding this comment.
Exact-head re-review at 30facd6072796dc962054e690c7eca720dd8005c.
The two R1 mechanisms are substantially improved: all current package/src TypeScript seams now fail removal/misordering mutations, and audioPadTrim exact-redacts the argv input before generic scrubbing. The focused contract/redaction suites pass 48/48. I am still holding the stamp for two P2s:
P2 — the “discovery” contract excludes shipped runtime callers where the same bug is still live. ffprobeArgvContract.test.ts:23-24,68-105 only walks packages/*/src, only .ts, and excludes tests. It therefore cannot discover .mjs/.cjs/shell skill code. Concrete current user-path calls still missing -- immediately before input include:
skills/figma/scripts/verify-motion.mjs:47-55,63-73(reference/render)skills/motion-graphics/grounding/locate.mjs:39-49(img)skills/{faceless-explainer,pr-to-video,product-launch-video}/scripts/assemble-index.mjs:82-86(abs)skills/media-use/scripts/lib/probe.mjs:13-16,audio/scripts/lib/tts.mjs:109-114,dither.mjs:120-124, andtranscript-cut.mjs:202-213
I reproduced the same ffprobe failure here: a final -customer-secret-intro.mp4 without the terminator is parsed as an option and returns Missing argument for option 'customer-secret-intro.mp4'. These are distributed agent tools, not fixtures. Either sweep/discover the shipped skill callers too, or narrow the “every invocation / everywhere / bug class cannot recur” contract and land an explicit immediately-linked skill follow-up; as written the new green test gives repo-wide false confidence while existing user-facing paths remain broken.
P2 — the exact-path redaction is wired but not regression-tested at the caller, and a sibling error still returns the raw path. Every new assertion is against redactTelemetryString / redactKnownPaths in isolation. Removing audioPadTrim.ts:469-470 leaves all of them green; no test drives runFfprobeJson with real stderr and asserts the public PadTrimAudioResult.error omits the dash-prefixed/relative/arbitrary-root input. That is the exact wiring R1 asked to pin. Also defaultProbeVideoFrameInfo still throws ffprobe found no video stream in ${videoPath} at audioPadTrim.ts:392, which flows through failResult at :238-245 without the known-path scrub. Add one public-path regression and route all probe failures through the same known-input sanitizer.
CI note: package Test/Build/Lint/Typecheck and focused local tests are green, but exact-head Tests on windows-latest is currently red on an unrelated Studio 50k-fixture 5s timeout; regression shards 1-8 are still pending. The Windows failure does not explain either finding, but the gate is not terminal green yet.
|
New head P2-1 — discovery excluded shipped skill callers. Confirmed and worse than the list: sweeping found 19 sites missing the terminator, not 9. Beyond the ones you named, The sweep now covers Two guard bugs surfaced while widening, both caught before they shipped:
Mutation-tested: removing P2-2 — redaction wired but not pinned at the caller. Your check reproduced exactly: deleting the Four public-path regressions drive Suites: core 1453, producer 529, cli 2325, lint 511, studio-server 398, engine 1299 — all green. Lint and format clean on the 21 changed files. Re-requesting review. |
…site #2740 added `--` to one of nine independent ffprobe invocations, so the bug class it closed stayed open everywhere else while CI reported it fixed — the regression test asserts the argv of that single site. Reproduced on ffprobe 8.1.1: an asset named `-intro.mp4` probes fine through extractMediaMetadata but fails with "Missing argument for option 'intro.mp4'" in audio pad/trim (mid-render), `hyperframes init`, whisper duration probing and webmAlphaCheck. hevcPreviewLint catches and returns false, so a dash-prefixed HEVC preview silently passes the lint rule. Terminated at all of them: producer/services/render/audioPadTrim.ts (x2) producer/plan-parity-analysis.ts cli/commands/init.ts cli/utils/webmAlphaCheck.ts cli/whisper/transcribe.ts (x2) core/mediaGradeAnalyzer.ts lint/hevcPreviewLint.ts audioPadTrim's runFfprobeJson is a near-verbatim clone of the engine's runFfprobe and structurally cannot add the terminator itself, because callers bake the input path into `args`. It now asserts the terminator is present rather than letting a dash-prefixed path through, takes the same stdio ["ignore", ...] as the engine helper, and redacts its stderr — it was throwing raw ffprobe output, which echoes the input path, into logs and telemetry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ontract The previous commit claimed "all nine now terminate their options". That was false: `producer/src/utils/audioRegression.ts:307` still passed the path bare, and it is production source used by the regression harness. A repo-wide audit found two more in studio-server (`mediaValidation.ts`, `mediaMetadata.ts`) — their current callers pass absolute paths, so they were defence-in-depth rather than live bugs, but the exhaustiveness claim should be true rather than narrowed. Eleven sites total, all terminated. Adds a SOURCE-level contract test, which is the gap that let this happen twice. #2740 fixed one of ten sites and shipped a regression asserting the argv of that single site, so CI reported the class closed while nine invocations still parsed `-intro.mp4` as an option. A per-site unit test has the same blind spot for site twelve; scanning the tree does not. The test also asserts its own coverage list has not shrunk. Verification: engine 1300, lint 511, core 1431, studio-server 398, cli init/webmAlphaCheck/whisper 146, producer utils 51, audioPadTrim 18. Removing any single terminator fails the contract test by name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prior contract test scanned a hardcoded file list for a format flag followed by a bare identifier, so it only matched the shape it was written against. Mutation testing showed removing `--` from engine/utils/ffprobe.ts and cli/commands/init.ts did not fail it. Now walks packages/*/src and finds callers itself, checks that `--` is immediately BEFORE the input rather than merely present, and compares discovery against a manifest so a regex regression cannot silently stop checking a known caller. A separate guard fails on any file that spawns a probe binary but builds an argv this test cannot parse. All 11 seams mutation-tested for both removal and misordering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
redactTelemetryString enumerated roots — /Users, /home, /opt, /tmp and a handful more — so a project on /data, /Volumes, an NFS mount or any root a user invented reached telemetry verbatim. Relative paths and bare basenames were never redacted at all, and audioPadTrim routes raw ffprobe stderr through this on every probe failure. Now redacts by shape: absolute paths under any root (two or more segments, so N/A and a 24/1 frame rate are not mistaken for one), relative paths including dash-prefixed ones, and bare basenames with an asset extension. URLs still keep their host and drop only the query. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The generic scrub still missed a relative path with no `./` prefix: `customer/acme-secret/video.mp4` and `assets/bgm.mp3` reached telemetry completely unredacted, because the absolute rule needs a leading slash and the `./` rule needs the dot. Adds a rule for them that still leaves `N/A`, `24/1` and `48000/1001` alone. Shape matching is a net with holes by construction, so audioPadTrim now also redacts the exact path it put in the argv, plus its basename, before the generic scrub runs. It built the argv, so it does not have to guess. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The contract test only walked packages/*/src and only .ts, so it could not see
the shipped agent tools under skills/**, which are .mjs/.cjs. 19 call sites
there and in package tests were still missing `--` immediately before the
input while the suite reported the bug class closed — a dash-prefixed filename
is parsed as an option and fails the same way.
Sweeps packages/, skills/ and scripts/ now, including .mjs/.cjs and test
files (dither.test.mjs was one of the broken sites). Excludes only the
contract test itself, which documents the contract with example argvs
including a deliberately misordered one.
Two guards were fixed while widening: the terminator must never be inserted
after `-i`, which consumes the next token (a blind pass hit an ffmpeg input
and a base64 -i), and comment prose describing a spawn is not a spawn.
Also routes every audioPadTrim probe failure through one sanitizer at the
boundary. runFfprobeJson scrubbed its own stderr, but
defaultProbeVideoFrameInfo threw `no video stream in ${videoPath}` raw into
the public PadTrimAudioResult.error, and an injected probe can throw anything.
The redaction unit tests all passed with the caller wiring deleted; the new
public-path regressions fail without it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI runs the PR merged with main, so it saw a caller my branch predated:
`spawnSync("ffprobe", ["-version"])` in engine/src/utils/ffprobe.test.ts. That
is a capability check with no runtime path, so there is nothing to terminate,
but the unclassified guard flagged it as a caller it could not parse.
An argv whose entries are all string literals carries no input by
construction. Those are dropped before the check; an argv with a bare
identifier still has to be understood, verified by adding one and watching the
guard fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ce8b929 to
c81f68b
Compare
CodeQL flagged js/redos on the all-literal argv matcher. It was right: the `(?:"[^"]*"\s*,?\s*)+` form nests a quantifier inside a quantifier with an optional separator, so whitespace can be matched two ways and a long non-matching argv backtracks exponentially. Replaced with a linear scan — find the spawn head, slice to the closing bracket, and check the entries — plus small named helpers. Same behaviour: an all-literal argv is treated as taking no input, an argv with a bare identifier still has to be understood (verified by adding one and watching the guard fail). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Follow-up on Rebased onto main (was 109 behind). CI tests the PR merged with main, which is how it saw a caller my branch predated: CodeQL caught a ReDoS I introduced ( Pre-existing failures, for the record. After rebasing, Current head is green on everything except the regression shards, still running. Suites on this head: producer 594, engine 1370, and the |
miguel-heygen
left a comment
There was a problem hiding this comment.
Reviewed at 6c5403f7. The JavaScript/TypeScript callers from R2 are fixed, caller-level audioPadTrim redaction is now wired, and the ReDoS alert is correctly removed with a linear scan. I am still requesting changes for three concrete issues:
-
[P2] The "whole-tree" ffprobe contract still excludes shipped shell/Python callers.
ffprobeArgvContract.test.ts:32-35,132-160walksskills/but only admits.ts/.js/.mjs/.cjs.skills/remotion-to-hyperframes/scripts/frame_strip.sh:52-57embeds Python and passes the user-controlledbaselinedirectly as ffprobe's last positional argument without"--". A file named-client.mp4is parsed as an option: locally the current argv fails withMissing argument for option 'client.mp4'; inserting--makes ffprobe treat it as the input. The same scan also ignores four live ffprobe calls inskills/embedded-captions/scripts/render-and-composite.sh:355-381. Those happen to canonicalize their paths first, so they are not immediately exploitable, but they disprove the "everywhere"/discovery guarantee. Either include the runtime shell/Python surfaces in the gate or narrow the claim and add a separate contract for them;frame_strip.shitself needs the terminator. -
[P2] The new boundary sanitizer throws when an injected probe rejects a non-
Errorvalue. The comment ataudioPadTrim.ts:223-227correctly says caller-supplied probes can throw anything, but lines 263-276 cast the rejection toErrorand pass.messageintoredactKnownPaths. ForPromise.reject("probe failed"), that value isundefined;redactKnownPathsthen throws at itsout.split(...)call. Before this change the function returned a failedPadTrimAudioResult(with an uglyundefinedmessage) rather than rejecting. Normalize first:reason instanceof Error ? reason.message : String(reason), and pin both injected probe seams with a non-Error rejection. -
[P2/privacy] The new "any absolute root" redactor still emits Unicode paths verbatim.
telemetryRedaction.ts:18-21,45-47,65-66builds path segments from ASCII\w. Exact current-head repro:redactTelemetryString("ffprobe failed reading /数据/客户/秘密视频.mp4")returns the input unchanged;/data/客户/secret.mp4also remains unchanged.redactKnownPathsprotects the two audioPadTrim paths because that caller knows them, but the generic redactor is also used for CLI telemetry and producer observation messages where no known-path list is supplied. Use Unicode-aware segment classes (or a delimiter-based path rule) and add non-Latin absolute/relative regressions.
Focused local evidence: the current contract suite passes 63/63 and telemetry-redaction suite 23/23, which confirms these cases are outside the present tests. Core CI, producer unit/integration, Test, Fallow, lint, typecheck, and CodeQL are green; three regression shards were still running when reviewed.
… callers
Three R3 findings.
The redactor's segment classes were ASCII `\w`, so `/数据/客户/秘密视频.mp4` and
`/data/客户/secret.mp4` went out verbatim — and the generic redactor also feeds
CLI telemetry and producer observation messages, where no known-path list
compensates. Segments are now defined by their delimiters instead of an
alphabet, which is correct for every script by construction rather than
requiring Unicode classes to be kept correct. The bare-relative lookbehind had
the same ASCII assumption and let a match start mid-token, redacting
`客户/秘密/视频.mp4` to `客户[path]`; it is now a token boundary, and
bare-relative runs before absolute so it claims the whole token.
sanitizeProbeFailure cast the rejection reason to Error and read `.message`.
An injected probe can reject with anything, so `Promise.reject("failed")` gave
`undefined` and threw inside the redactor — converting a returned failure
result into a rejected promise. Normalized at the boundary, and
redactKnownPaths no longer throws on a non-string.
The contract only admitted .ts/.js/.mjs/.cjs, so it missed shipped shell and
Python callers. frame_strip.sh passed a user-controlled path as ffprobe's last
positional with no terminator; render-and-composite.sh had four more. Both
fixed, and the sweep now covers .py/.sh. Python list argvs are bracket
literals so they get the same position check; shell command lines get a
separate presence check, because checking position there needs a shell parser
— stated as the weaker guarantee it is rather than implied to be equal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
New head 1 — shell/Python callers excluded. Confirmed. On the gate, I took your first option but split it by what's actually checkable:
2 — non-Error rejection threw. Reproduced exactly: 3 — Unicode paths verbatim. Reproduced all three of your examples. Rather than enumerate That exposed a second instance of the same ASCII assumption: the bare-relative lookbehind was Verified the non-paths still survive: Mutation-tested all three seams — reverting each fix fails the suite. Suites: core 1497, producer 598, engine 1370, cli 2436, lint 514, studio-server 406. Re-requesting review. |
miguel-heygen
left a comment
There was a problem hiding this comment.
Approving at 1664fe6.
All three findings from my previous review are closed at the actual boundaries:
- Unicode path redaction now defines segments by delimiters rather than ASCII word characters. The exact prior repros redact fully, including /数据/客户/秘密视频.mp4, /data/客户/secret.mp4, /Volumes/客户 项目/秘密.mov, and /tmp/用户/private.mov.
- audioPadTrim normalizes unknown rejection reasons before sanitizing, so string, undefined, null, number, and plain-object probe rejections return a failed PadTrimAudioResult instead of rejecting from the redactor. redactKnownPaths also fails soft on a non-string.
- The source sweep now includes .py and .sh. The embedded Python argv in frame_strip.sh is checked for penultimate terminator position; direct shell lines are checked separately for terminator presence. All five shipped shell/Python misses are fixed.
Verification:
- telemetry redaction plus argv contract: 102/102 focused tests pass locally.
- Mutation check: removing the frame_strip.py terminator and one render-and-composite.sh terminator produced two named contract failures; restoring them returned 67/67.
- Exact-head Producer unit CI is green, covering the public audioPadTrim rejection regressions.
- Worktree is clean and HEAD is exactly 1664fe6.
CI is still running: 44 success, 2 skipped, 12 pending, 0 failures at review time. Approval clears the code-review block; required checks still gate merge.
Review by Magi.
Replaces #2917, which was closed unmerged. Same content, re-verified. Independent of #2916 — rebased directly onto
main.Completes #2740, and adds the thing that stops this bug class coming back a third time.
The gap
#2740 added
--to one of ten independent ffprobe invocations. Its regression test asserted the argv of that single site, so CI reported the class closed while nine invocations still parsed a path like-intro.mp4as an option.Reproduced on ffprobe 8.1.1:
producer/services/render/audioPadTrim.ts×2Missing argument for option 'intro.mp4'— mid-rendercli/commands/init.tshyperframes initcli/whisper/transcribe.ts×2cli/utils/webmAlphaCheck.tscore/mediaGradeAnalyzer.tsproducer/plan-parity-analysis.tslint/hevcPreviewLint.tsproducer/utils/audioRegression.tsstudio-server/mediaValidation.ts,mediaMetadata.tsEleven sites, all terminated.
The part that matters
ffprobeArgvContract.test.ts. A per-site unit test has exactly the blind spotthat let this happen twice — #2740 asserted its one site, and my own sweep then
missed a site and shipped no argv test at all.
The first version of this test had the same disease in a new shape: it scanned a
hardcoded file list for a format flag followed by a bare identifier, so it
only matched the shape it was written against. Review was right to reject it —
mutation testing showed that removing
--fromengine/utils/ffprobe.ts,cli/commands/init.tsandcli/whisper/transcribe.tsdid not fail it.What it does now:
packages/*/srcrather than trusting a list,and discovery is argv-shaped, not call-shaped. A call-shaped predicate
(
spawn/execFile) missedstudio-server/mediaValidation.ts, which invokesthrough an injected
runner("ffprobe", …).["-of","json",path,"--"]contains theterminator and does nothing; a presence check passes it.
caller, its assertions stop running and the suite still goes green — the exact
silent-vacuum failure the two previous versions had.
builds an argv the matcher doesn't understand is reported, not skipped.
Mutation-tested at all 11 seams, for both removal and misordering. Killing
argvTailsoutright fails the manifest test, so the guard is itself guarded.The clone that couldn't take the fix
audioPadTrim.ts'srunFfprobeJsonis a near-verbatim copy of the engine'srunFfprobeand structurally cannot add--itself, because callers bake theinput path into
args. It now asserts the terminator is present, takes the samestdio: ["ignore", ...]as the engine helper, and redacts its stderr — itwas throwing raw ffprobe output, which echoes the input path, into logs,
telemetry, and
PadTrimAudioResult.error.Collapsing that duplication into the engine helper is worth doing, but not here:
the two have diverged in error-message prefix and caller contract.
The redaction was not actually redacting
redactTelemetryStringenumerated roots —/Users,/home,/opt,/tmpanda few more. Everything else went out verbatim: a project on
/dataor/Volumes, any NFS mount, every relative path, and every bare basename. Reviewdemonstrated
customer/acme-secret/video.mp4surviving untouched, and it wasright.
Two changes, because one is not enough:
./-prefixed and bare relativepaths, and basenames with an asset extension. Two separators (or one plus an
extension) are required, so
N/A, a24/1frame rate and48000/1001stillread normally. URLs keep their host and lose only the query.
redactKnownPathstakes the exact path the caller put in theargv, plus its basename, and removes it before any pattern runs. Shape matching
is a net with holes by construction; a caller that built the argv doesn't have
to guess.
Verification
Full suites green on every touched package: engine 1299, cli 2325, core 1453,
lint 511, producer 491, studio-server 398. Typecheck and lint clean across all
six.
Every fix here has a regression that fails when that fix alone is reverted —
verified individually, including the contract test's own discovery guard.
🤖 Generated with Claude Code