Security: argument/protocol injection into FFmpeg via generateSubtitle MCP mediaUrl (CWE-78 / CWE-918)
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, ...])insideextractAudioWithFFmpeg - Source:
mediaUrlargument of thegenerateSubtitleMCP tool (Zod-validated asz.string(), no scheme constraint)
Data flow
- MCP tool
generateSubtitleis registered withgenerateSubtitleSchema = z.object({ mediaUrl: z.string(), language: z.string().optional() })—z.string()does not restrict the URL scheme. - The handler forwards
mediaUrlunchanged intoextractAudioWithFFmpeg(mediaUrl). extractAudioWithFFmpegcallsexeca('ffmpeg', ['-i', mediaUrl, ...]).execaavoids 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-inmediaUrlis additionally parsed by FFmpeg as a flag.
Proof of concept
Any caller that can reach the MCP tool can invoke it with:
{
"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 filessubfile,,start,0,end,4096,,:/etc/shadow— byte-range read of a local filehttp://169.254.169.254/latest/meta-data/— SSRF to cloud metadatagopher://internal-service:6379/_...— SSRF against internal Redis/etc.-yor-loglevel debugas the value ofmediaUrl— 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:
- Parse with
new URL(mediaUrl)and requireprotocol === 'http:' || 'https:'. - Reject any value starting with
-(defence against argument injection). - Additionally pass
-protocol_whitelist https,http,tls,tcpto 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
generateSubtitleMCP 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-aicontainer, SSRF into the internal network (including cloud metadata endpoints), and potential FFmpeg-flag injection.
Source: yikart/AiToEarn