Security and Operations for Media Processing
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. See the section bibliography for the reference material consulted while preparing these pages. |
Baseline date: 2026-10-07. A media pipeline feeds untrusted files to some of the most complex parsers in a typical stack: image decoders, demuxers and codecs. Those parsers have a long history of memory-safety bugs, and they run with whatever rights the worker has. The controls below limit what an attacker can submit, what a bug can reach, and what the pipeline costs when somebody abuses it. They apply to every language page in this section.
Validate what was uploaded
-
Magic bytes, not extension or
Content-Type. Both are chosen by the client. Read the first bytes and check them against the format you expect (JPEG starts withFF D8 FF, PNG with89 50 4E 47, WebP withRIFF....WEBP, ISO media files such as AVIF, HEIC and MP4 haveftypat offset 4). Libraries do this for you:ImageIO.getImageReaders, Pillow’sImage.openwithformatandverify, sharp’smetadata(), the Rustimagecrate’swith_guessed_format, andffprobefor video. Reject anything that is not on your allow list. -
Allow list of formats. Accept the formats you can handle, not "everything FFmpeg can read". FFmpeg and ImageMagick support hundreds of formats, and some of them (playlists, SVG with external references) can read local files or make network requests. In FFmpeg, restrict the protocols (
-protocol_whitelist file) and the formats (-f/-formatsselection) for user input, and do not feed user-provided URLs to ImageMagick or FFmpeg at all (SSRF). -
Size and duration caps. Limit the object size in the pre-signed URL, in the HTTP server and in the worker. Probe a video first (
ffprobe) and refuse files above your maximum duration, resolution and frame rate before you transcode. -
Decompression bombs. A tiny file can decode to gigabytes of pixels (a 1 MB PNG of 60000 x 60000 pixels). Check the declared width and height from the header before you decode, and cap pixels (Pillow’s
Image.MAX_IMAGE_PIXELSraisesDecompressionBombError, the Rustimagecrate hasLimits, libvips decodes lazily with sequential access, and ImageSharp can read the size without decoding throughImage.IdentifyAsync). Videos can be bombs too: a short clip with extreme resolution. -
Virus scanning. If the files are shared with other users (documents, archives), scan the upload with an engine such as ClamAV or the cloud provider’s malware protection before it is made available.
Strip metadata
EXIF holds GPS coordinates, device model and serial numbers, timestamps and sometimes a thumbnail of the original, including regions you cropped out. Strip it from every public variant:
| Tool | How |
|---|---|
sharp |
Metadata is stripped by default. |
libvips, pyvips, NetVips |
Metadata is kept by default. Pass |
Pillow |
Saving without |
Thumbnailator |
Written images carry no EXIF unless you copy it. |
ImageSharp |
EXIF loaded from the source is written back on save. Set |
FFmpeg |
|
Apply the orientation to the pixels first, then strip the tag, see image formats and encoding. Keep the original in private storage if you need it, and treat it as personal data.
Sandbox and limit the workers
-
Least privilege. The worker’s identity can read
uploads/, writemedia/and update its own status, and nothing else. No broad storage or database credentials. -
Isolation. Run FFmpeg and image libraries in a container with a read-only root filesystem, no network egress (they do not need it, which also blocks SSRF and data exfiltration), a non-root user, dropped capabilities and a seccomp profile. For stronger isolation, use gVisor or a microVM runtime, or run each job in its own short-lived container or cloud function, see Docker security and Kubernetes pod security.
-
Resource limits. Set CPU and memory limits, a process time limit (
timeout,ulimit, the language’s process timeouts), a disk quota for the scratch directory and a maximum number of processes. A single hostile or merely unlucky file must not exhaust a node. See Kubernetes resource management. -
No shell. Pass arguments to FFmpeg and
vipsas an array, never concatenated into a shell string, and never use the user’s file name on the command line. Use generated names (<uuid>.<ext>). A file name starting with-is an option. -
Delete scratch files. Clean the temporary directory after each job, and on startup.
Keep it patched
Decoders are a common source of CVEs. Track the releases and security advisories of FFmpeg, libvips, libheif, libwebp, libavif, libjpeg-turbo, ImageMagick (if you use it) and of the language bindings that bundle them (sharp ships its own libvips, Pillow wheels bundle libraries, JAVE2 bundles FFmpeg binaries). Rebuild the worker image regularly, scan it (see image best practices and security), and pin versions so that an update is a deliberate change. libwebp had a widely exploited heap overflow in 2023 (CVE-2023-4863), which is the example of why bundled libraries matter.
Storage layout, cost and lifecycle
-
Separate prefixes or buckets for
uploads/(private, short retention),originals/(private), andmedia/(served through the CDN). The CDN has read access tomedia/only. -
Lifecycle rules: delete
uploads/after processing (a few days is enough to debug), expire incomplete multipart uploads, and move originals to a cheaper storage class. See the lifecycle sections in S3, Cloud Storage and Blob Storage. -
Cost drivers: storage for every variant of every image (hence the short list of widths), egress at the CDN (hence AVIF and WebP), compute for the encodes (hence the slow AVIF encode on a queue, and spot or preemptible instances for video), and per-minute fees of managed transcoders. Prices are not quoted here, use the provider calculators.
-
Do not process the same bytes twice: key outputs by a content hash and skip files already processed (deduplication).
-
Rate-limit uploads per user and per tenant, and cap the number of concurrent jobs per tenant, so one user cannot fill the queue.
Monitoring
Track these metrics and alert on them:
| Metric | Why |
|---|---|
Queue depth and age of the oldest message |
The main signal that workers are behind. Alert when age exceeds your target. |
Job duration (histogram) per recipe, and per input resolution for video |
Detects regressions and slow encodes. Compare against the input duration. |
Success, failure and retry counts, by error type |
Corrupt uploads are normal. A jump in one error type is not. |
Dead-letter queue depth |
Anything there needs a human. |
Worker CPU, memory and out-of-memory kills |
Right-size the limits. |
Output size versus input size |
A sudden drop or growth shows an encoder setting problem. |
Time from upload to |
The user-visible metric. |
Use structured logs with the media id and job id in each line, and traces across API, queue and worker, as described in observability for Kubernetes workloads and the messaging pages linked from upload pipelines.