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 with FF D8 FF, PNG with 89 50 4E 47, WebP with RIFF....WEBP, ISO media files such as AVIF, HEIC and MP4 have ftyp at offset 4). Libraries do this for you: ImageIO.getImageReaders, Pillow’s Image.open with format and verify, sharp’s metadata(), the Rust image crate’s with_guessed_format, and ffprobe for 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/-formats selection) 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_PIXELS raises DecompressionBombError, the Rust image crate has Limits, libvips decodes lazily with sequential access, and ImageSharp can read the size without decoding through Image.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. keepMetadata(), keepExif() or withMetadata() keep it.

libvips, pyvips, NetVips

Metadata is kept by default. Pass keep=none on save (strip on libvips older than 8.15).

Pillow

Saving without exif= drops it. Apply ImageOps.exif_transpose first.

Thumbnailator

Written images carry no EXIF unless you copy it.

ImageSharp

EXIF loaded from the source is written back on save. Set image.Metadata.ExifProfile = null, and clear the XMP and IPTC profiles, before saving.

FFmpeg

-map_metadata -1 drops global metadata, -fflags +bitexact avoids the encoder tag. Check the location tag of phone videos (com.apple.quicktime.location.ISO6709).

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/, write media/ 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 vips as 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), and media/ (served through the CDN). The CDN has read access to media/ 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 ready

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.