Go

How do you run external commands safely with os/exec? Explain CommandContext, capturing stdout/stderr, exit codes, and why shell injection is not the default risk.

Question 540HardGo 1.22 to 1.25

exec.Command(name, args...) runs the program directly (execve or CreateProcess). No shell is involved, so ;, |, $() and globs inside arguments are just literal bytes. Injection only comes back if you write exec.Command("sh", "-c", userString). The remaining risk is argument injection: a user value starting with - can be read as a flag (--upload-pack=...), so put -- before positional arguments. On Windows, argument quoting is done by the target program, so be extra careful with cmd.exe and .bat files. Since Go 1.19, LookPath refuses executables that it finds relative to the current directory (exec.ErrDot).

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

cmd := exec.CommandContext(ctx, "git", "log", "--oneline", "--", userPath)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.WaitDelay = 2 * time.Second // Go 1.20: bound waiting for I/O after kill

err := cmd.Run()
var exitErr *exec.ExitError
switch {
case ctx.Err() != nil:
	log.Printf("timed out: %v", ctx.Err())
case errors.As(err, &exitErr):
	log.Printf("exit code %d: %s", exitErr.ExitCode(), stderr.String())
case err != nil:
	log.Printf("could not start: %v", err) // e.g. exec.ErrNotFound
default:
	fmt.Print(stdout.String())
}

CommandContext kills the process when the context is cancelled. Since Go 1.20 you can customize this with cmd.Cancel, for example to send SIGTERM first. cmd.Output() captures stdout and, if Stderr is nil, stores stderr in ExitError.Stderr. CombinedOutput merges the two. For large outputs, use StdoutPipe, but finish reading before calling Wait. Child processes can still inherit your environment unless you set cmd.Env.

More on More Standard Library Essentials

All 16 More Standard Library Essentials questions