From 291a360a83331bbe470bd86d5f1a815a1e38752e Mon Sep 17 00:00:00 2001 From: Mathias Date: Thu, 1 Oct 2026 17:14:29 +0200 Subject: [PATCH] feat(audio): restrict ffmpeg/ffprobe input to local files and audio demuxers The upload is attacker-supplied and its CONTENT can steer ffmpeg to open other files or URLs (HLS/concat playlists). Pins -protocol_whitelist file,pipe and -format_whitelist to the containers real recordings use, before the input (after -i it would apply to the output). Verified with the real static ffmpeg 7.0.2 on koala: real AAC .m4a and WAV still probe/convert; a crafted concat file is rejected with 'Format not on whitelist'. Note ffmpeg 7.0.2 ALREADY refused the playlists tried without the whitelist, so the live check cannot prove the whitelist -- the argv unit tests do; this protects against a different ffmpeg version being put in the image. Co-Authored-By: Claude Sonnet 5.5 --- internal/adapters/audio/ffmpeg.go | 6 ++++-- internal/adapters/audio/ffprobe.go | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/internal/adapters/audio/ffmpeg.go b/internal/adapters/audio/ffmpeg.go index ee04971..609cd50 100644 --- a/internal/adapters/audio/ffmpeg.go +++ b/internal/adapters/audio/ffmpeg.go @@ -27,8 +27,10 @@ func (f *FFmpeg) ToWAV(ctx context.Context, src, dst string) error { if bin == "" { bin = "ffmpeg" } - cmd := exec.CommandContext(ctx, bin, - "-y", "-v", "error", "-i", src, "-vn", "-ac", "1", "-ar", "16000", "-c:a", "pcm_s16le", "-f", "wav", dst) + // Input restrictions go BEFORE -i: placed after it they would apply to the output. + args := append([]string{"-y", "-v", "error"}, inputRestrictions...) + args = append(args, "-i", src, "-vn", "-ac", "1", "-ar", "16000", "-c:a", "pcm_s16le", "-f", "wav", dst) + cmd := exec.CommandContext(ctx, bin, args...) cmd.WaitDelay = killGrace if out, err := cmd.CombinedOutput(); err != nil { if ctxErr := ctx.Err(); ctxErr != nil { diff --git a/internal/adapters/audio/ffprobe.go b/internal/adapters/audio/ffprobe.go index d4d27b6..5b75b08 100644 --- a/internal/adapters/audio/ffprobe.go +++ b/internal/adapters/audio/ffprobe.go @@ -21,6 +21,19 @@ type FFprobe struct { Bin string } +// AllowedDemuxers are the input formats an upload may be. The input is +// attacker-supplied, and a file's CONTENT can tell ffmpeg to open other files or +// URLs (HLS and concat playlists, image2 sequences, network demuxers). Recent +// ffmpeg refuses the ones tried (7.0.2: HLS without a .m3u8 name is not sniffed, +// concat rejects unsafe paths), but that is version luck; this list is not. It +// holds the containers real recordings use and none of the playlist or network +// formats. "mov" covers .mp4/.m4a/.3gp; "matroska" covers .webm. +const AllowedDemuxers = "wav,mov,mp4,m4a,3gp,mp3,ogg,matroska,webm,flac,aac,opus,aiff,amr,asf" + +// inputRestrictions are ffmpeg/ffprobe INPUT options and must precede the input. +// Local files and pipes only: no network, no other protocols. +var inputRestrictions = []string{"-protocol_whitelist", "file,pipe", "-format_whitelist", AllowedDemuxers} + // killGrace bounds how long Probe waits for a cancelled ffprobe's pipes to close. const killGrace = 2 * time.Second @@ -34,8 +47,9 @@ func (f *FFprobe) Probe(ctx context.Context, path string) (uploads.Probe, error) if bin == "" { bin = "ffprobe" } - cmd := exec.CommandContext(ctx, bin, - "-v", "error", "-print_format", "json", "-show_format", "-show_streams", path) + args := append([]string{"-v", "error"}, inputRestrictions...) + args = append(args, "-print_format", "json", "-show_format", "-show_streams", path) + cmd := exec.CommandContext(ctx, bin, args...) cmd.WaitDelay = killGrace out, err := cmd.Output() if err != nil {