Back to Blog
critical SEVERITY7 min read

ffmpeg.Probe() Protocol Abuse in the GetSnapshot Thumbnailer

The local storage driver's thumbnail helpers — `GetSnapshot()` and `resizeImageToBufferWithFFmpegGo()` — handed user-controlled paths to `ffmpeg.Input()` and `ffmpeg.Probe()` with no restriction on which FFmpeg protocols those processes could open. A file that passed the existing `sanitizeFilePath` check (absolute, cleaned, regular file) could still be an HLS playlist or concat script pointing at `http://`, `subfile:` or `file:` targets, turning thumbnail generation into SSRF and arbitrary file

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published October 13, 2026•Reviewed October 13, 2026

Answer Summary

This is a first-party fix in a Go local storage driver's FFmpeg helpers: `GetSnapshot(videoPath)` and `resizeImageToBufferWithFFmpegGo(inputFile, width, outputFormat)`. Because neither invocation restricted FFmpeg's protocol handlers, a user who could place a file in storage could make the server's `ffprobe`/`ffmpeg` child processes fetch attacker-chosen URLs (cloud metadata endpoints, internal services) or read local files and render them into the returned thumbnail. The fix passes `-protocol_whitelist file` as a global argument to both `ffmpeg.Input()` pipelines and supplies `ffmpeg.KwArgs{"protocol_whitelist": "file"}` to `ffmpeg.Probe()`; no package version applies since this is first-party code. The finding was classified as CWE-78 (OS command/argument injection into a subprocess).

Vulnerability at a Glance

cweCWE-78
fixRestrict both ffmpeg and ffprobe invocations to the `file` protocol via `-protocol_whitelist file`
riskThumbnail generation can be coerced into fetching internal HTTP endpoints or reading local files and leaking them through the generated image or probe output
languageGo
root cause`ffmpeg.Input()` and `ffmpeg.Probe()` received user-controlled paths with no `-protocol_whitelist`, so FFmpeg treated them as URLs and followed nested references
vulnerabilityArgument/protocol injection into an FFmpeg subprocess (SSRF + arbitrary file read)

Summary

The local storage driver's thumbnail helpers — GetSnapshot() and resizeImageToBufferWithFFmpegGo() — handed user-controlled paths to ffmpeg.Input() and ffmpeg.Probe() with no restriction on which FFmpeg protocols those child processes could open. A file that passed the existing sanitizeFilePath check (absolute, cleaned, regular file) could still be an HLS playlist or concat script pointing at http://, subfile: or file: targets, turning thumbnail generation into SSRF and arbitrary file read. The fix adds -protocol_whitelist file to every FFmpeg invocation and passes ffmpeg.KwArgs{"protocol_whitelist": "file"} to ffmpeg.Probe().

Introduction

ffmpeg.Input("/srv/storage/user42/clip.mp4") looks like a filesystem operation. It isn't. FFmpeg's -i argument is a URL, and FFmpeg resolves it through a long list of protocol handlers: file:, http:, https:, tcp:, rtmp:, pipe:, concat:, subfile:, data:, and more. The path you think you're opening is really a string that a very capable network client is about to parse.

That mismatch is the whole bug here. The local storage driver generates thumbnails for media it finds on disk. Two entry points do the work:

  • resizeImageToBufferWithFFmpegGo(inputFile string, width int, outputFormat string) — builds ffmpeg.Input(inputFile).Output("pipe:", outputArgs) and streams the result into a bytes.Buffer.
  • GetSnapshot(videoPath string) — calls ffmpeg.Probe(videoPath) to read the container duration, computes a seek offset, then runs ffmpeg.Input(videoPath, ffmpeg.KwArgs{"ss": ss, "noaccurate_seek": ""}) with an image2/mjpeg output to grab one frame.

GetSnapshot does run its argument through a sanitizeFilePath helper, which checks that the path is absolute, cleaned, and refers to a regular file. That stops ../../etc/shadow and it stops a literal http://evil.test/x string from being accepted as a path. What it cannot do is inspect what's inside the file — and FFmpeg's container formats are full of indirection that points back out to the network. If you write code that spawns FFmpeg against anything a user can influence, this section is about your code too.

Affected Versions

Affected not applicable (first-party code) — the local storage driver's FFmpeg thumbnail helpers (GetSnapshot, resizeImageToBufferWithFFmpegGo)
Fixed in not applicable (first-party code) — fixed by the referenced hardening commit that adds -protocol_whitelist file
Ecosystem go
CVE / GHSA not assigned
CWE CWE-78 (OS command/argument injection into a subprocess)

The Vulnerability Explained

The vulnerable pattern

Before the fix, the image resize path built its command like this:

err = ffmpeg.Input(inputFile).
    Output("pipe:", outputArgs).      // Output to pipe (stdout)
    GlobalArgs("-loglevel", "error").
    Silent(true).
    WithOutput(outBuffer, os.Stderr).
    Run()

And the snapshot path probed the container like this:

jsonOutput, err := ffmpeg.Probe(videoPath)

Two global arguments are present (-loglevel error) and one crucial one is absent: there is nothing telling FFmpeg which protocol handlers it may use. The same omission appears in the frame-extraction stream that follows the probe.

Note that this is not classic shell metacharacter injection — ffmpeg-go builds an argv slice and does not go through /bin/sh, so a ; or $(…) in a filename is inert. The injection happens one layer in: the attacker controls a value that the callee re-interprets as a URL and as a reference to further URLs. The security boundary everyone assumes exists ("it's just a path") was never enforced.

Attacking this code path

The attacker needs only the ability to put a file somewhere the driver will thumbnail, plus the ability to request that thumbnail. Two concrete variants:

1. SSRF via an HLS playlist. Upload a file named clip.m3u8 (or any extension the thumbnailer accepts — FFmpeg probes by content, not suffix) containing:

#EXTM3U
#EXT-X-TARGETDURATION:10
#EXTINF:10,
http://169.254.169.254/latest/meta-data/iam/security-credentials/
#EXT-X-ENDLIST

sanitizeFilePath passes it: absolute, clean, regular file. ffmpeg.Probe() then has ffprobe resolve the segment over HTTP from inside your network. The request originates from the application server, with its security groups and its instance identity. Even when the response isn't valid media, timing and the error text returned up the err path from ffmpeg.Probe make a serviceable oracle for internal port and host scanning.

2. Local file disclosure rendered into the thumbnail. The subfile: and concat: protocols, and the concat demuxer, let an input stitch together byte ranges of other files. Point one at a credentials file or a private key, let the image2/mjpeg output encode the bytes as a frame, and download the thumbnail your own API cheerfully returns. The exfiltration channel is the product feature itself.

Because GetSnapshot runs ffmpeg.Probe before the frame extraction, the first attempt alone is enough to trigger an outbound fetch — an attacker gets SSRF even if the subsequent decode fails.

Real-world impact

For a self-hosted file manager or media service, this means any user with write access to a watched directory can reach cloud metadata endpoints, unauthenticated internal admin panels, and container-network services, and can read server-side files into an image they're authorized to download. On a cloud instance with an attached role, step one is usually credential theft from the metadata service. That's why this is rated critical despite needing a low-privilege foothold.

The Fix

The change is small and surgical: constrain what FFmpeg and ffprobe are permitted to open.

Before — the probe inherits no protocol restrictions:

jsonOutput, err := ffmpeg.Probe(videoPath)

After — the probe is pinned to the file protocol:

// Restrict ffprobe to the file protocol only, so a crafted path cannot
// make it fetch remote resources or read arbitrary local files via
// other protocols (http, concat, subfile, pipe, etc.).
jsonOutput, err := ffmpeg.Probe(videoPath, ffmpeg.KwArgs{"protocol_whitelist": "file"})

The same restriction is added to both encoding pipelines as a global argument. In the resize helper:

GlobalArgs("-loglevel", "error").
GlobalArgs("-protocol_whitelist", "file"). // Restrict ffmpeg to the file protocol only
Silent(true).

And in the snapshot stream that carries ffmpeg.KwArgs{"ss": ss, "noaccurate_seek": ""}:

GlobalArgs("-loglevel", "error").
GlobalArgs("-protocol_whitelist", "file").Silent(true).

Why all three edits were necessary

Each FFmpeg invocation is a separate operating-system process with its own argv. ffmpeg.Probe() spawns ffprobe; the Input(...).Output("pipe:") chains spawn ffmpeg. Hardening one does nothing for the others — and in GetSnapshot the probe runs first, so leaving it unrestricted would have preserved the SSRF primitive even with the frame extractor locked down. Three call sites, three identical restrictions.

With -protocol_whitelist file in effect, a playlist segment of http://169.254.169.254/... is rejected by FFmpeg's protocol layer before a socket is opened, and concat:, subfile:, pipe: and data: inputs are refused outright. The existing sanitizeFilePath call still earns its keep — it keeps the top-level path absolute and cleaned — but it is now backed by a control that operates at the layer where the actual decision is made.

One honest caveat, and the reason this is hardening rather than a complete seal: the concat demuxer (-f concat) still operates over the file protocol. It depends on FFmpeg's default -safe 1 to reject absolute and otherwise unsafe entries, so that default must not be overridden, and the process should continue to run with the narrowest filesystem permissions the deployment allows. The upstream change was submitted as a reviewed suggestion with no automated verification run against the repository, so maintainers should confirm the thumbnail paths still behave as expected for legitimate local media.

Key Takeaways

  • ffmpeg.Input() and ffmpeg.Probe() take URLs, not paths. Any string you pass is eligible for FFmpeg's http:, concat:, subfile:, pipe: and data: handlers unless you explicitly narrow them with -protocol_whitelist.
  • A sanitizeFilePath that checks "absolute, cleaned, regular file" validates the wrong thing. It inspects the path string, while the attack lives in the file's contents — a legitimate-looking .m3u8 or concat script is a regular file that points somewhere else.
  • Protocol restrictions must be applied per process. ffmpeg.Probe() forks ffprobe with its own argv, so GlobalArgs("-protocol_whitelist", "file") on an Input(...).Output(...) chain never reaches it; the probe needs ffmpeg.KwArgs{"protocol_whitelist": "file"} of its own.
  • Thumbnails are an exfiltration channel. An image2/mjpeg output encodes whatever bytes the demuxer fed it, so arbitrary-read primitives surface as a downloadable image through your normal API.
  • A probe that runs before decoding is a full SSRF primitive by itself. In GetSnapshot, the duration lookup fires first — hardening only the frame-extraction stream would have left the hole open.

How Orbis AppSec Detected This

  • Source: user-influenced filesystem paths flowing into the inputFile parameter of resizeImageToBufferWithFFmpegGo() and the videoPath parameter of the local driver's GetSnapshot() — values derived from directory traversal over storage that users can write to.
  • Sink: the FFmpeg subprocess invocations ffmpeg.Input() (built into ffmpeg/ffprobe argv and executed) and ffmpeg.Probe().
  • Missing control: no restriction on FFmp

Prevention and further reading

Frequently Asked Questions

Does the existing `sanitizeFilePath` check (absolute, cleaned, regular file) stop this attack?

No. It validates the path string and that the target is a regular file, but says nothing about the file's *contents* — a 200-byte `.m3u8` playlist or concat script is a perfectly ordinary regular file whose segments ffmpeg will happily open over `http://`.

Why does `ffmpeg.Probe()` need its own `protocol_whitelist` KwArgs instead of inheriting the fix from the ffmpeg pipeline?

`ffmpeg.Probe()` spawns a separate `ffprobe` process with its own argv, so the `GlobalArgs("-protocol_whitelist", "file")` added to the `ffmpeg.Input(...).Output("pipe:")` chain never reaches it. It has to be passed explicitly as `ffmpeg.KwArgs{"protocol_whitelist": "file"}`.

With `-protocol_whitelist file` in place, can a crafted input still read other local files?

Remote protocols and `pipe:`/`subfile:`/`concat:` are blocked, but the concat *demuxer* still uses the `file` protocol; it relies on FFmpeg's default `-safe 1` to reject absolute and unsafe paths, so do not override that setting, and keep the thumbnailer running with least-privilege filesystem access.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #9666

Related Articles

critical

webapp-testing Server Script: Shell Injection via Unquoted Command

A critical command injection vulnerability in a web server testing utility allowed attackers to execute arbitrary shell commands by injecting metacharacters into command-line arguments. The fix removes shell interpretation and safely tokenizes user-supplied commands using `shlex.split()`, while preserving support for legitimate directory-change workflows.

high

THiNX Device Platform: Command Injection via source.owner in Git Fetch

The THiNX device management platform fixed a critical vulnerability where the `source.owner` parameter flowed directly into shell commands during git repository operations. An attacker with source configuration access could inject arbitrary shell commands by providing malicious owner values containing metacharacters like `; curl`.

critical

backtest.js CLI args: Unvalidated --days and --start Options

The backtest.js command-line tool accepted --days and --start arguments without validation, creating a command injection vector when the script is invoked by upstream processes with untrusted input. The fix adds strict input validation: --days must be a positive integer ≤ 3650, and --start must match the YYYY-MM-DD date format.

critical

Tauri type_text() Command Injection via Control Characters in

The type_text() system command handler in Tauri accepted arbitrary text up to 2000 UTF-16 characters without validating content, allowing injection of control characters that SendInput's Unicode path interprets as real key presses. An attacker could inject newline, escape, or tab characters to trigger actions in the focused window beyond mere text typing.

high

runStreaming() Command Injection: Defense-in-Depth for Electron Child

An Electron application's `runStreaming()` utility accepted a command string and argument array without validating either, creating a latent command injection vector. The fix adds strict type checking and a whitelist regex that rejects shell metacharacters, bounding the failure mode even if caller input becomes attacker-influenced.

critical

CVE-2026-59873: node-tar 7.5.11 DoS via Crafted Gzip Bomb

node-tar versions 7.5.11 through 7.5.18 are vulnerable to a denial-of-service attack through maliciously crafted gzip archives that decompress to disproportionately large sizes. An attacker can exploit this to exhaust memory and CPU resources by submitting a small, highly compressed archive that expands beyond configured limits during extraction.