Skip to main content

Understanding command injection vulnerabilities in Go

Written by:
feature-insights-context

November 14, 2024

0 mins read

Go developers might need to use system commands for various scenarios, such as image manipulation, where they need to process or resize images or execute system commands to manage resources or gather metrics or logs.

At other times, perhaps you are building a new system in Go that needs to interface with existing legacy systems. This interface leans on executing system commands and processing their output.

In either case, it is essential to follow secure coding conventions when spawning system commands, as this could lead to a security vulnerability known as command injection.

What is command injection?

Command injection is a security vulnerability when an application passes unsafe user-supplied data (such as input from a web form) to a system shell. This weakness allows an attacker to execute arbitrary commands on the host operating system under the same application user.

In Go, a command injection often involves using the os/exec package to spawn system commands.

Consider the following Go code example:

func handler(req *http.Request) {
  cmdName := req.URL.Query()["cmd"][0]
  cmd := exec.Command(cmdName)
  cmd.Run()
}

In this code snippet, the cmdName is extracted directly from the request query parameters and used to construct a command that is executed on the server. This is a classic example of command injection vulnerability because an attacker can manipulate the cmd query string value to execute any command they choose, potentially compromising the server.

An attacker could craft a request with a malicious command, such as:

http://example.com/execute?cmd=rm%20-rf%20/

Why is command injection dangerous? Command injection is dangerous because it allows attackers to execute arbitrary commands on the server, which can lead to severe consequences, including:

  • Data Breach: Attackers can access sensitive data stored on the server. Imagine them accessing configuration files such as config.toml and others that list resources and credentials.

  • System Compromise: Attackers can gain control over the server, leading to further exploitation. This often takes the form of lateral movements within a compromised system and discovery actions for other servers that could be compromised.

  • Service Disruption: Attackers can execute commands that disrupt services, causing downtime. Effectively this could directly translate to financial losses and business risk due to a denial of service attack.

The impact of a successful command injection attack can be devastating, affecting not only the compromised system but also the organization's reputation and customer trust.

Vulnerable process execution and command injection in Go

Go developers, known for their preference for simplicity and performance, might choose to integrate system commands to harness the capabilities of these utilities. This approach allows them to focus on building robust applications without reinventing the wheel. However, this integration comes with its own set of challenges, particularly in terms of security.

To illustrate a more realistic scenario, consider a Go application that processes image files using a command-line utility like convert.

We explore a Go application designed to handle image resizing requests. The application uses the Gin web framework to define a POST endpoint, /cloudpawnery/image, which processes image resizing based on user input. This endpoint accepts parameters such as tenantID, fileID, and fileSize from the query string. The fileSize parameter is optional and defaults to "200" if not provided.

The following code snippet demonstrates a vulnerable implementation in Go.

func main() {
    // Create a Gin router
    router := gin.Default()

    // Define a POST endpoint
    router.POST("/cloudpawnery/image", func(c *gin.Context) {
        tenantID := c.Query("tenantID")
        fileID := c.Query("fileID")
        fileSize := c.Query("fileSize")

        if fileSize == "" {
            fileSize = "200"
        }

        // Validate tenantID and fileID
        if tenantID == "" || fileID == "" {
            c.JSON(http.StatusBadRequest, gin.H{"error": "Missing tenantID or fileID"})
            return
        }

        // Call the download and resize function
        err := downloadAndResize(c, tenantID, fileID, fileSize)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }

        // Return a success response
        c.JSON(http.StatusOK, gin.H{"message": "File downloaded and resized successfully"})
    })

    // Start the HTTP server
    router.Run(":7000")
}

func downloadAndResize(ctx *gin.Context, tenantID, fileID, fileSize string) error {
    slog.Info("Processing request", "tenantID", tenantID, "fileID", fileID)

    convertCmd := fmt.Sprintf("convert %s -resize %sx%s %s", targetFilename, fileSize, fileSize, targetFilename)
    slog.Info("Running command", "command", convertCmd)

    _, err := exec.CommandContext(ctx, "sh", "-c", convertCmd).CombinedOutput()
    if err != nil {
        slog.Error("Error resizing image", "error", err)
        return fmt.Errorf("%w: %v", ErrImageResize, err)
    }

    slog.Info("Downloaded and resized image", "filename", targetFilename)
    return nil
}

The downloadAndResize function constructs a command string to resize the image using convert and executes it using exec.CommandContext.

How does the downloadAndResize function construct a command string? It takes this input using user-provided fileSize. This string is then executed, potentially allowing an attacker to inject malicious commands. To mitigate this risk, Go developers should validate and sanitize all user inputs, use parameterized commands, and leverage security practices that safely handle command execution.

Mitigating command injection vulnerabilities

In Go development, just like in any other language, ensuring your code is secure against command injection vulnerabilities is paramount. Command injection occurs when an attacker can execute arbitrary commands on the host which can lead to unauthorized access, data breaches, and other severe security issues.

Let's explore some best practices to mitigate these risks.

Input validation and sanitation

One of the foundational steps to prevent command injection is rigorous input validation and sanitation. In the provided example, the downloadAndResize function constructs a command string using user-supplied inputs like fileSize. If these inputs are not properly validated, an attacker could inject malicious commands.

Here's how you can improve input validation:

  1. Allow-list inputs: Define a set of acceptable values for inputs like fileSize. For instance, only allow numeric values that fall within a reasonable range.

  2. Sanitize inputs: Remove or escape any potentially dangerous characters from user inputs. This can include shell metacharacters such as ;, |, &, etc. It is best advise that you hard-code the command and do not allow user-input to control it.

  3. Use strict types: Convert inputs to the expected data type as soon as possible. For example, convert fileSize to an integer and validate its range.

Here's an example of how you might implement these practices:

func sanitizeInput(input string) (int, error) {
    size, err := strconv.Atoi(input)
    if err != nil || size <= 0 || size > 1000 {
        return 0, fmt.Errorf("invalid input")
    }
    return size, nil
}

func downloadAndResize(ctx *gin.Context, tenantID, fileID, fileSize string) error {
    size, err := sanitizeInput(fileSize)
    if err != nil {
        return fmt.Errorf("invalid file size: %w", err)
    }

    convertCmd := fmt.Sprintf("convert %s -resize %dx%d %s", targetFilename, size, size, targetFilename)
    // ...
}

 Even still, we can do a lot better to secure Go code from command injection. Keep reading!

Use of safe APIs or libraries instead of system commands

Another effective strategy is to avoid executing system commands directly whenever possible. Instead, leverage safe APIs or libraries that provide the required functionality without exposing your application to command injection risks.

For example, if your application needs to manipulate images, consider using a Go library like github.com/disintegration/imaging instead of calling an external command like convert from the ImageMagick software library. This approach encapsulates the functionality within Go's type-safe environment, reducing the attack surface.

import (
    "github.com/disintegration/imaging"
    // ...
)

func resizeImage(inputPath, outputPath string, width, height int) error {
    img, err := imaging.Open(inputPath)
    if err != nil {
        return fmt.Errorf("failed to open image: %w", err)
    }

    resizedImg := imaging.Resize(img, width, height, imaging.Lanczos)
    err = imaging.Save(resizedImg, outputPath)
    if err != nil {
        return fmt.Errorf("failed to save image: %w", err)
    }

    return nil
}

By using libraries like imaging in Go, you eliminate the need to construct and execute shell commands, thereby mitigating the risk of command injection. Still, in some libraries and language ecosystems, it is possible that the third-party package is a simple wrapper around command execution so it is mandatory to review code for such sensitive operations.

Refactor the vulnerable downloadAndResize function to prevent injection

In the previous example, we demonstrated a potentially vulnerable Go application that uses the exec.CommandContext function to execute shell commands. This approach can lead to command injection vulnerabilities as we noted.

Let’s attempt to refactor the downloadAndResize function to ensure that user input does not lead to arbitrary command execution.

One effective way to prevent command injection is to avoid constructing shell command strings directly from user input. Instead, we can use the exec.Command function with separate arguments, which helps in safely passing user input as parameters to the command without invoking the shell and without allowing users to control the actual command.

Here's a refactored version of the downloadAndResize function that addresses the command injection vulnerability:

func downloadAndResize(ctx *gin.Context, tenantID, fileID, fileSize string) error {
    slog.Info("Processing request", "tenantID", tenantID, "fileID", fileID)

    // Define the command and its arguments separately
    args := []string{targetFilename, "-resize", fmt.Sprintf("%sx%s", fileSize, fileSize), targetFilename}
    cmd := exec.CommandContext(ctx, "convert", args...)

    slog.Info("Running command", "command", cmd.String())

    // Execute the command
    _, err := cmd.CombinedOutput()
    if err != nil {
        slog.Error("Error resizing image", "error", err)
        return fmt.Errorf("%w: %v", ErrImageResize, err)
    }

    slog.Info("Downloaded and resized image", "filename", targetFilename)
    return nil
}

In this refactor we separated the command from its arguments. By using exec.CommandContext with separate arguments, we avoid the need to construct a shell command string. This method ensures that user input is treated as data rather than executable code, significantly reducing the risk of command injection.

We also removed the need for shell invocation. The refactored code does not invoke the shell (sh -c), which is a common vector for command injection. Instead, it directly calls the convert utility with specified arguments.

Leveraging Snyk for code security

Snyk Code is a powerful static analysis tool that helps developers identify and fix vulnerabilities in their codebase. It integrates seamlessly into your IDE and your development workflow, providing real-time feedback on potential security issues.

How Snyk can help identify command injection vulnerabilities in Go

In the provided Go example, the downloadAndResize function constructs a shell command using user-supplied input:

convertCmd := fmt.Sprintf("convert %s -resize %sx%s %s", targetFilename, fileSize, fileSize, targetFilename)

_, err := exec.CommandContext(ctx, "sh", "-c", convertCmd).CombinedOutput()

This code is vulnerable to command injection because it directly incorporates user input into the command string.

What if your team had developers who weren’t aware of command injection vulnerabilities?

Would you easily be able to pinpoint the inter-file static analysis call flow in a code review to find this command injection vulnerability?

This is where Snyk comes in.

Look at how application security becomes easy with the Snyk Code extension high-lighting in real-time vulnerable code within the VS Code editor:

1_Understanding_Command_Injection_vulnerabilities_in_Go

Snyk can help identify such vulnerabilities by scanning your Go code and flagging instances where user input is used unsafely in shell commands. Snyk shows you real commits made by other open-source projects that mitigated this specific vulnerability so you get several code references as examples of what “good looks like”.

Even more so, if you click the ISSUE OVERVIEW tab you get a drill-down of more details and best practices for prevention of command injection. This tab provides detailed insights and recommendations on how to mitigate these risks and it is available straight in the IDE view while you code and can follow these actions without a costly context switch:

2_Understanding_Command_Injection_vulnerabilities_in_Go

For further learning on how to secure your code against such vulnerabilities, consider installing the Snyk Code IDE extension and connecting your Git projects to identify and fix security issues in your Go applications. You can do it easily and free by signing up to start scanning your code for vulnerabilities.

feature-insights-context

Level Up Your CI/CD Pipelines

See how these 8 tips can help you catch security issues in the pipe BEFORE you push to production ⭐️