fix(media): stop the ffmpeg output format from choosing its own path - #7001
Conversation
`convert` and `extract_audio` built their output path as
`path.join(dir, `out.${format}`)`, and `format` arrives from the tool as an
unconstrained `{type: 'string'}` — no pattern, no enum, and the handler passes it
through untouched. A format of `../../../../../../../../tmp/x.mp4` resolves to
`/tmp/x.mp4`, so FFmpeg writes attacker-influenced media wherever the traversal
points. Verified against a real binary: the run produced a 44,078-byte MP4
outside the temp directory, which the `rm -rf` cleanup then never saw, because
the file was never inside the directory being removed.
Output extensions are now letters and digits only. The pattern admits no `.` and
no separator, so `out.${ext}` is always a single path segment and containment
follows from the validation itself rather than from a second check that could
drift away from it.
`trim` and `fade` build their paths from `extFromMime`, which returns
`mime.split('/')[1]` — that can never contain a separator, so those stay inside
the temp dir and keep accepting values like `x-msvideo` that this stricter
pattern would wrongly reject.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryHigh Risk Overview
Reviewed by Cursor Bugbot for commit 88e3235. Configure here. |
Greptile SummaryThis PR prevents caller-controlled FFmpeg output formats from escaping the operation’s temporary directory.
Confidence Score: 5/5The PR appears safe to merge, with the caller-controlled output path contained across the affected FFmpeg operations. The alphanumeric extension constraint excludes dots, path separators, NUL bytes, and Windows drive syntax, and both caller-controlled format paths now use the validated helper before FFmpeg starts.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/media/ffmpeg.ts | Adds sound cross-platform output-extension validation before constructing caller-influenced FFmpeg paths. |
| apps/sim/lib/media/ffmpeg.test.ts | Covers malicious formats, both affected operations, accepted formats, and uppercase normalization without introducing test-state issues. |
Reviews (1): Last reviewed commit: "fix(media): stop the ffmpeg output forma..." | Re-trigger Greptile
Summary
convertandextract_audiobuilt their output path aspath.join(dir, `out.${format}`), andformatarrives from the tool as an unconstrained{"type": "string"}— nopattern, noenum— which the handler passes through untouched. A traversal in that string relocates the file FFmpeg writes.Verified against a real FFmpeg binary rather than reasoned about:
The temp directory is removed with
fs.rm(dir, { recursive: true, force: true })afterwards, which never sees that file — it was never inside the directory being removed. So the write also outlives the cleanup that is supposed to bound this tool's filesystem footprint.The bytes are a transcode of a caller-supplied input, so the attacker influences the content as well as the destination, bounded only by what the app process can write. Reaching it needs nothing but workspace write permission, the same gate as the rest of the tool.
Fix
Output extensions are letters and digits only, up to 12 characters. Because the pattern admits no
.and no separator,out.${ext}is always a single path segment — containment follows from the validation itself rather than from a second check that could drift away from it.Every ordinary target still works:
mp4,mp3,webm,m4a,flac,opus,gif,mkv, andMP4still lowercases before validating.What is deliberately not changed
trimandfadealso build paths from an extension, but theirs comes fromextFromMime, which returnsmime.split('/')[1]— that can never contain a separator, so those paths are already contained. Routing them through this stricter pattern would wrongly reject legitimate values likex-msvideothat the fallback produces for an unrecognized MIME type.Provenance
Found while tracing a report that targeted
escapeDrawtextandaddText. That vulnerability is already fixed — #6734 replaced the inline caption withtextfile=plusexpansion=none, andescapeDrawtextno longer exists — but the report's observation that the tool's string parameters are unconstrained still held forformat, which is a path rather than a filtergraph.Verification
Six traversal cases pinned by tests — parent-relative, deep-relative, embedded
.., absolute, and an embedded NUL — each confirmed to fail against the unfixed source before the fix was applied. Full suite: 1,789 tests acrosslib/copilotandlib/media, plustsc --noEmit,lint:check, and theapi-validation/boundaries/client-boundary/utilsgates.Follow-ups, not in scope
-protocol_whiteliston FFmpeg inputs would stop a crafted input container from referencing external resources. No injection vector remains through the tool's own parameters, so this is defence in depth rather than a fix, and it needs care not to break the concat demuxer.concatis separately broken for any clip without an audio stream: it synthesizes silence with-f lavfi, butlavfiis a libavdevice device and fluent-ffmpeg validates-fagainstffmpeg -formats, which does not list devices. Reproduced standalone; fix verified; wants its own PR.🤖 Generated with Claude Code