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.
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
- Q538Why should you compare secrets with crypto/subtle.ConstantTimeCompare instead of ==? How do you hash passwords in Go?
- Q539How does regexp (RE2) differ from PCRE engines? Why doesn't Go support backreferences, and what are the performance guarantees?
- Q541How do you build a TCP server with net.Listen? Handle per-connection goroutines, read/write deadlines, and graceful shutdown of the listener.
- Q542Explain io/fs and fs.FS. How do you walk a directory with filepath.WalkDir and test file-system code with testing/fstest.MapFS?
- Q543What is os.Root (Go 1.24), and how does it prevent path-traversal attacks compared to filepath.Join plus filepath.Clean?
- Q544How do you parse CLI flags with the flag package? How do subcommands work, and why is flag.Parse in init() a bug?