Go Command Line Argument Injection via exec.Command

Critical Risk Command Injection
gocommand-injectionexecshell-injectionrce

What it is

Command injection vulnerabilities occur when user-controlled input is passed directly to exec.Command without proper validation, allowing attackers to inject malicious shell commands and execute arbitrary code on the server.

package main

import (
    "fmt"
    "net/http"
    "os/exec"
)

func pingHandler(w http.ResponseWriter, r *http.Request) {
    host := r.URL.Query().Get("host")
    
    // VULNERABLE: User input passed to shell
    cmd := exec.Command("sh", "-c", "ping -c 4 "+host)
    output, err := cmd.CombinedOutput()
    
    if err != nil {
        http.Error(w, "Command failed", 500)
        return
    }
    
    w.Write(output)
}

func fileHandler(w http.ResponseWriter, r *http.Request) {
    filename := r.URL.Query().Get("file")
    
    // VULNERABLE: String concatenation with user input
    cmd := exec.Command("sh", "-c", fmt.Sprintf("cat %s", filename))
    output, err := cmd.CombinedOutput()
    
    if err != nil {
        http.Error(w, "File read failed", 500)
        return
    }
    
    w.Write(output)
}

func main() {
    http.HandleFunc("/ping", pingHandler)
    http.HandleFunc("/file", fileHandler)
    http.ListenAndServe(":8080", nil)
}
package main

import (
    "fmt"
    "net/http"
    "os/exec"
    "regexp"
)

// Allowlist of valid hosts
var allowedHosts = map[string]bool{
    "localhost":   true,
    "google.com":  true,
    "example.com": true,
}

var hostnamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9.-]{0,61}[a-zA-Z0-9]$`)

func pingHandler(w http.ResponseWriter, r *http.Request) {
    host := r.URL.Query().Get("host")
    
    // SECURE: Validate against allowlist
    if !allowedHosts[host] || !hostnamePattern.MatchString(host) {
        http.Error(w, "Invalid host", 400)
        return
    }
    
    // SECURE: Use argument array, no shell
    cmd := exec.Command("ping", "-c", "4", host)
    output, err := cmd.CombinedOutput()
    
    if err != nil {
        http.Error(w, "Ping failed", 500)
        return
    }
    
    w.Write(output)
}

var allowedFiles = map[string]bool{
    "readme.txt":  true,
    "config.json": true,
    "log.txt":     true,
}

func fileHandler(w http.ResponseWriter, r *http.Request) {
    filename := r.URL.Query().Get("file")
    
    // SECURE: Validate against allowlist
    if !allowedFiles[filename] {
        http.Error(w, "File not allowed", 400)
        return
    }
    
    // SECURE: Use argument array, no shell
    cmd := exec.Command("cat", filename)
    output, err := cmd.CombinedOutput()
    
    if err != nil {
        http.Error(w, "File read failed", 500)
        return
    }
    
    w.Write(output)
}

func main() {
    http.HandleFunc("/ping", pingHandler)
    http.HandleFunc("/file", fileHandler)
    http.ListenAndServe(":8080", nil)
}

💡 Why This Fix Works

The secure version validates all user input against allowlists, uses exec.Command with argument arrays instead of shell strings, and completely avoids shell interpreters to prevent command injection.

Why it happens

Go applications pass user-controlled input directly to exec.Command with shell interpreters like 'sh -c' or 'bash -c', allowing attackers to inject shell metacharacters and execute arbitrary commands. This bypasses Go's normal command/argument separation.

Root causes

Direct User Input to Shell Commands

Go applications pass user-controlled input directly to exec.Command with shell interpreters like 'sh -c' or 'bash -c', allowing attackers to inject shell metacharacters and execute arbitrary commands. This bypasses Go's normal command/argument separation.

String Concatenation for Command Building

Developers build shell commands using string concatenation or fmt.Sprintf with user input (e.g., cmd := 'ls ' + userInput). This creates a single shell string where special characters like ;, |, &&, and $() enable command injection.

Missing Input Validation and Sanitization

No validation or sanitization applied to user input before including it in command execution. Applications fail to check for dangerous characters, validate against allowlists, or escape shell metacharacters before passing input to exec.Command.

Improper Argument Array Usage

Not leveraging exec.Command's variadic argument array to separate the command from its arguments. When arguments aren't passed as separate array elements, Go concatenates them into a shell string, enabling injection through special characters.

Fixes

1

Eliminate Direct User Input to Commands

Never pass user-controlled input directly to any command execution functions. If user input is necessary, validate it against a strict allowlist of permitted values before use. Design application logic to avoid requiring user input in command construction entirely whenever possible.

2

Use Separated Argument Arrays

Use exec.Command(name, arg1, arg2, ...) with separate argument parameters instead of shell strings. This prevents shell interpretation of special characters. For example, use exec.Command('ls', '-la', userDir) rather than exec.Command('sh', '-c', 'ls -la ' + userDir).

3

Implement Strict Input Allowlists

For any user-controllable parameters that must be included in commands, implement strict allowlists of permitted values. Use switch statements or map lookups to validate input against known-good values, rejecting anything that doesn't exactly match an approved entry.

4

Validate with Restrictive Regex Patterns

Apply regular expression validation to ensure user input contains only safe characters (e.g., ^[a-zA-Z0-9_-]+$). Reject any input containing shell metacharacters like ;, |, &, $, `, \, or spaces. Implement multiple validation layers for defense-in-depth.

5

Avoid Shell Interpreters Entirely

Never invoke shell interpreters (sh, bash, cmd.exe) when executing commands with user input. Call programs directly using exec.Command without shell wrappers. If shell features are required, reimplement the functionality in Go code rather than risking command injection.

Detect This Vulnerability in Your Code

Sourcery automatically identifies go command line argument injection via exec.command and many other security issues in your codebase.