#579·AiToEarn

Security: argument/protocol injection into FFmpeg via generateSubtitle MCP mediaUrl (CWE-78 / CWE-918)

Author: andesyteossCreated Jul 27, 2026Updated Jul 27, 2026

Summary

The generateSubtitle MCP tool in apps/aitoearn-ai/src/core/agent/mcp/subtitle.mcp.ts passes a caller-supplied mediaUrl directly to ffmpeg -i without validating the URL scheme. Because FFmpeg accepts many protocols beyond http(s) (file:, concat:, subfile:, data:, pipe:, rtmp:, gopher:, …), any authenticated MCP caller — including an LLM/agent that a user has connected via the MCP server — can coerce the server to read arbitrary local files, perform SSRF against internal services, or inject FFmpeg CLI flags.

  • CWE: CWE-78 (argument injection into an external program) / CWE-918 (SSRF) / CWE-22 (arbitrary file read)
  • File: project/aitoearn-backend/apps/aitoearn-ai/src/core/agent/mcp/subtitle.mcp.ts
  • Sink: execa('ffmpeg', ['-i', mediaUrl, ...]) inside extractAudioWithFFmpeg
  • Source: mediaUrl argument of the generateSubtitle MCP tool (Zod-validated as z.string(), no scheme constraint)

Data flow

  1. MCP tool generateSubtitle is registered with generateSubtitleSchema = z.object({ mediaUrl: z.string(), language: z.string().optional() })z.string() does not restrict the URL scheme.
  2. The handler forwards mediaUrl unchanged into extractAudioWithFFmpeg(mediaUrl).
  3. extractAudioWithFFmpeg calls execa('ffmpeg', ['-i', mediaUrl, ...]). execa avoids a shell (so this is not classic shell injection), but FFmpeg itself parses the URL and dispatches to a protocol handler (file, concat, data, subfile, rtmp, gopher, pipe, srtp, …). A leading - in mediaUrl is additionally parsed by FFmpeg as a flag.

Proof of concept

Any caller that can reach the MCP tool can invoke it with:

json
{
  "name": "generateSubtitle",
  "arguments": {
    "mediaUrl": "file:///etc/passwd",
    "language": "en"
  }
}

FFmpeg opens /etc/passwd (or any readable file on the server) as an input. Equivalent payloads that all reproduce against the pre-fix code:

  • concat:/etc/hosts|/etc/passwd — read+concatenate local files
  • subfile,,start,0,end,4096,,:/etc/shadow — byte-range read of a local file
  • http://169.254.169.254/latest/meta-data/ — SSRF to cloud metadata
  • gopher://internal-service:6379/_... — SSRF against internal Redis/etc.
  • -y or -loglevel debug as the value of mediaUrl — argument injection into the FFmpeg command line

FFmpeg output feeds the transcription pipeline; even without direct output, error messages and blind/timing side channels are typically enough to exfiltrate contents (and concat: frequently returns file bytes verbatim to the audio stage).

Fix

Validate mediaUrl before it ever reaches FFmpeg:

  1. Parse with new URL(mediaUrl) and require protocol === 'http:' || 'https:'.
  2. Reject any value starting with - (defence against argument injection).
  3. Additionally pass -protocol_whitelist https,http,tls,tcp to FFmpeg as defence-in-depth against protocol chaining inside playlists/HLS segments FFmpeg might follow.

Both extractAudioWithFFmpeg and the tool entry point call the validator so the check cannot be bypassed by future callers.

A PR with the fix will follow this issue: it adds an assertSafeMediaUrl helper and the -protocol_whitelist flag. The diff is a single file, +39 lines.

Preconditions & impact

  • Requires the ability to invoke the generateSubtitle MCP tool (authenticated MCP/agent user, or a prompt-injectable LLM connected to the server).
  • Does not require any pre-existing local-file-read capability on the host — this fix closes a real capability gap rather than duplicating existing access.
  • Impact: arbitrary local file read on the aitoearn-ai container, SSRF into the internal network (including cloud metadata endpoints), and potential FFmpeg-flag injection.