Command injection in Go happens when untrusted input shapes the command that runs, most often by passing it to a shell with sh -c, or as an argument that a program treats as an option. Go's os/exec does not invoke a shell by default, so avoiding shell wrappers removes the biggest risk.
What is command injection?
MITRE's CWE-78 describes it as constructing all or part of an OS command using externally influenced input without neutralizing special elements that could modify the intended command. The danger is greatest in web applications, where attackers have no other path to the operating system.
What does Go do differently?
The os/exec documentation says that, unlike the system call in C and other languages, the package intentionally does not invoke the system shell and does not expand globs or handle pipelines and redirection. Command takes a program name and a list of arguments, so each argument is passed as a separate value:
cmd := exec.Command("convert", userFile, "out.png")
A filename like "x; rm -rf /" is passed to convert as one odd filename, not interpreted by a shell. That one design choice prevents the classic semicolon attack.
Where do Go programs still go wrong?
The first mistake is bringing the shell back. Code that runs exec.Command("sh", "-c", "convert " + userFile) has reintroduced everything the package avoided. The documentation itself says that to expand globs you can call a shell directly, but you must take care to escape any dangerous input. Prefer filepath.Glob or doing the expansion in Go.
The second is argument injection. If a user supplies a value starting with a dash, the called program may treat it as an option. Validate against an allow list of expected values and, where the tool supports it, put a separator before user supplied values so they are treated as positional arguments.
The third is the program lookup. Command resolves a name with no path separator through LookPath. The documentation explains that since Go 1.19, LookPath and Command do not return a program found in the current directory through a relative path entry. Instead they return an error satisfying ErrDot. The docs say code that really wants a local program can write "./prog", and warn that overriding the error should happen only after understanding the security implications. Setting GODEBUG execerrdot to 0 restores old behavior and the docs say a future version may remove that variable.
Why does that matter? If your PATH includes the current directory, an attacker who can drop a file named like a common tool into a working directory could get it executed.
What is a safer pattern?
A constructed example: a service converts uploaded images. The vulnerable version builds a shell string with the filename. The safer version does four things. It generates its own temporary filename and never uses the uploaded name on the command line. It checks the file type against an allow list. It calls exec.Command with a full path to the tool and separate arguments. It runs under a context with a timeout and with a restricted user.
If you can avoid running a process at all, do it. A Go library for the same task removes the issue.
How to check or apply this
- Search the codebase for exec.Command and exec.CommandContext calls.
- Flag any call whose first argument is sh, bash or cmd, or that builds a command string.
- Pass arguments as separate values, never as a concatenated string.
- Validate user supplied values against an allow list before use.
- Prevent values from being read as options, using a separator where the tool supports it.
- Use full paths for tools, and do not suppress ErrDot without review.
- Use CommandContext with a timeout and run with minimal privileges.
- Add tests that pass hostile strings, such as semicolons and leading dashes.
Quick check
Does exec.Command protect me from all injection?
It protects against shell metacharacters because it does not use a shell. It does not stop argument injection or abuse of a shell you launch yourself.
What is ErrDot?
An error returned when a program lookup would resolve to the current directory through a relative path entry. It was added in Go 1.19 to prevent accidentally running a local file.
Sources
Last reviewed: 18 October 2026.