Skip to content

fix(media): stop the ffmpeg output format from choosing its own path - #7001

Merged
icecrasher321 merged 1 commit into
stagingfrom
fix/ffmpeg-output-format-traversal
Aug 23, 2026
Merged

fix(media): stop the ffmpeg output format from choosing its own path#7001
icecrasher321 merged 1 commit into
stagingfrom
fix/ffmpeg-output-format-traversal

Conversation

@icecrasher321

Copy link
Copy Markdown
Collaborator

Summary

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 — 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:

format = "../../../../../../../../tmp/ffx-escaped.mp4"
  path.join(dir, "out." + format)  ->  /tmp/ffx-escaped.mp4
  result: 44,078-byte MP4 written outside the temp directory

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, and MP4 still lowercases before validating.

What is deliberately not changed

trim and fade also build paths from an extension, but theirs comes from extFromMime, which returns mime.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 like x-msvideo that the fallback produces for an unrecognized MIME type.

Provenance

Found while tracing a report that targeted escapeDrawtext and addText. That vulnerability is already fixed#6734 replaced the inline caption with textfile= plus expansion=none, and escapeDrawtext no longer exists — but the report's observation that the tool's string parameters are unconstrained still held for format, 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 across lib/copilot and lib/media, plus tsc --noEmit, lint:check, and the api-validation / boundaries / client-boundary / utils gates.

Follow-ups, not in scope

  • A -protocol_whitelist on 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.
  • concat is separately broken for any clip without an audio stream: it synthesizes silence with -f lavfi, but lavfi is a libavdevice device and fluent-ffmpeg validates -f against ffmpeg -formats, which does not list devices. Reproduced standalone; fix verified; wants its own PR.

🤖 Generated with Claude Code

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

vercel Bot commented Aug 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 23, 2026 12:14am

Request Review

@cursor

cursor Bot commented Aug 23, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Security-sensitive path construction for FFmpeg output: a previously unconstrained tool parameter could write attacker-influenced media outside the temp dir. The change is a tight allowlist, but it sits on a filesystem write path.

Overview
Stops convert and extract_audio from treating the caller-supplied format string as a path. Previously path.join(dir, 'out.' + format) let a traversal write attacker-influenced media outside the temp dir, where cleanup never sees it.

outputPathForExt now requires a lowercase alphanumeric extension of 1–12 characters (^[a-z0-9]{1,12}$), so out.${ext} is always one path segment. Ordinary targets (mp4, mp3, webm, etc.) and MP4 still work. Tests cover parent-relative, absolute, embedded .., and NUL payloads, and assert FFmpeg is never started on reject.

Reviewed by Cursor Bugbot for commit 88e3235. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR prevents caller-controlled FFmpeg output formats from escaping the operation’s temporary directory.

  • Adds centralized validation restricting output extensions to 1–12 alphanumeric characters.
  • Applies validation to both convert and extract_audio.
  • Adds traversal, NUL-byte, ordinary-format, and case-normalization coverage.

Confidence Score: 5/5

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

Important Files Changed

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

@icecrasher321
icecrasher321 merged commit d4195ff into staging Aug 23, 2026
30 checks passed
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.

1 participant