Ahosting Logo
Knowledge Base

How to Process Untrusted Video Uploads Safely

Bounding what a malicious or malformed file can doDecoding untrusted input is running a parser on a stranger dataTimea timeout, because a craftedfile can make decodingeffectively endlessMemory and CPUlimits per job, so one filecannot take the machineDiska size cap on the output, sincea small input can produce a hugeonePrivilegesrun it as a user that ownsnothing elseIsolationa container or a separatemachine, if the volume justifiesitNever execute the uploadstore it outside the web root,alwaysValidate by decoding rather than by extension: the extension is chosen by whoever uploaded the file.

Accepting video uploads means taking a file chosen by a stranger and handing it to a large body of format-parsing code. That code is good and it has had vulnerabilities, as every decoder has.

The realistic goal is not to make processing unbreakable. It is to make a bad file cost you a failed job rather than a server.

Bound the time

timeout 300 ffmpeg -i input.mp4 ... output.mp4

A crafted file can make decoding take effectively forever. Without a limit that job holds a worker permanently, and a handful of them exhaust the queue.

Pick a limit from your real material with generous headroom, and treat exceeding it as a failure to report in place of a reason to retry.

Bound the memory

Some inputs cause very large allocations. When memory runs out the system starts killing processes, and it does not necessarily kill the one at fault: a database being terminated because of a video upload is a bad afternoon.

ulimit -v 2097152 # 2 GB, in the shell that runs the job

As a service, the limit belongs in the unit:

MemoryMax=2G
TasksMax=64

Running an application as a systemd service walks through the unit, and this is one of the better arguments for making the worker one.

Bound the privilege

Conversion should run as a dedicated user that owns nothing: no shell, no home directory worth reading, write access to one temporary directory and nothing else.

useradd -r -s /usr/sbin/nologin videoproc

If the worker runs as the web user, an exploit reaches every file the website owns: including its configuration and its database credentials. If it runs as root, it reaches everything.

This single separation is worth more than any amount of input validation.

Bound the protocols

FFmpeg can read from network locations as well as files, and some input formats are effectively playlists that name other inputs.

That means an uploaded file can potentially instruct the decoder to read a local path or make a request to an internal address: a server-side request forgery through a video upload.

ffmpeg -protocol_whitelist file,crypto -i input.mp4 ... out.mp4

Permit only what you actually need. For ordinary uploaded files that is file.

The extension means nothing

A file named .mp4 can contain anything. Trusting the name, or the type the browser reported, is trusting the uploader.

ffprobe -v error -show_entries format=format_name,duration -of csv input.mp4

Determine the real format, and reject anything that is not on your list. Also reject on duration and dimensions before converting. A video declaring enormous dimensions costs a great deal to process and is rarely legitimate.

Using ffprobe to inspect media files deals with reading what it reports.

Never build the command from user input

Generate your own filenames. A file named by the uploader, pasted into a shell command, is a command injection waiting to be found.

Store uploads under identifiers you created, keep the original name only as a label in the database, and pass paths as arguments rather than assembling a shell string.

Store uploads outside the web root

An uploaded file that is reachable by URL before it has been checked is a file being served to visitors on your behalf.

Keep the incoming directory above public_html, and publish only the converted output your own code produced. Setting up a file upload directory safely walks through the arrangement.

Verify what came out

Conversion can succeed and produce a valid video of nothing. Check the output has the duration and dimensions you expected, and is not entirely black.

ffmpeg -i out.mp4 -vf "blackdetect=d=1:pix_th=0.1" -f null - 2>&1 | grep black_start

Detecting scenes, silence and black frames goes over it, and this check catches ordinary failures as well as deliberate ones.

Keep the build current

Decoder vulnerabilities are fixed in releases you have to actually install. A build that shipped with the distribution three years ago has known issues.

Whatever else is done here, the version processing untrusted input should be a maintained one. Getting started with FFmpeg walks through installing a current build.

Bound the output as well as the input

The limits above constrain what the process may consume. They do not constrain what it may write.

A crafted input can produce an output far larger than itself: an enormous resolution, an absurd duration, or a stream that expands during conversion. The job stays within its memory and time limits and fills the disk instead.

ffmpeg -i input.mp4 -fs 500M -t 3600 ... output.mp4

-fs stops writing at a size limit and -t caps the duration regardless of the source. Both belong on any command processing files you did not create.

An account filled by a video conversion is not merely a failed job. It stops mail, database writes and sessions at the same moment. Understanding inodes walks through the related limit that is reached even sooner.

Reject before converting

The cheapest defence is refusing the file before any expensive work begins.

ffprobe -v error -select_streams v:0 \
 -show_entries stream=width,height,duration,codec_name \
 -of default=noprint_wrappers=1:nokey=1 input.mp4

Check the numbers against limits you decided in advance: a maximum resolution, a maximum duration, and a list of codecs you accept.

A file declaring an enormous resolution or a duration measured in days is almost never legitimate, and rejecting it costs milliseconds where converting it costs the machine. Using ffprobe goes into reading the output.

Clean up whatever the job leaves behind

A killed or timed-out job does not tidy up after itself. Partial outputs and temporary files accumulate in exactly the directory you are trying to protect.

find /var/tmp/videoproc -type f -mmin +120 -delete

Run that on a schedule rather than relying on jobs to clean up, because the jobs that need cleaning are the ones that did not finish.

Write each job's output to a temporary name and move it into place only on success. A partial file with the final name is worse than no file, because everything downstream treats it as complete. For the pattern, see building a batch processing pipeline.

Consider whether you need to accept video at all

The honest question, and it is worth asking before building any of the above.

Accepting video means running a large decoder against hostile input, indefinitely, on your own machine. For many sites the requirement is really "let people share a video", which an embedded link satisfies with none of this.

Where uploads are genuinely needed, a specialised service takes the processing and the risk. That is a cost in place of a failure, and it is a smaller cost than most people estimate against the machine time this consumes. There is more on the queue you would otherwise be running in handling video uploads from a web application.