Command Injection

Overview

Command injection (or OS command injection) is an attack in which the goal is the execution of arbitrary OS commands. Command injection attacks are possible when an application passes user-controlled data directly to the shell, allowing an attacker to inject and execute arbitrary commands.

For example, in the snippet below, if an attacker can control the ip variable, they will be able to execute arbitrary commands by passing the 1.1.1.1; id value to ip variable.

os.system(f'ping {ip}')
# if ip == '1.1.1.1; id' the shell command will be:
# ping 1.1.1.1; id

You can find more details at PortSwigger Web Security Academy: OS command injection.

This page contains recommendations for the implementation of protection against command injection attacks.

General

  • Do not call OS commands directly if there is a library or API for your language.

Clarification

It is better to use a built-in method to perform the required task than to call an OS command.

For example, use mkdir(name) instead of system('mkdir ${name}').

  • Use parameterized methods that allow automatically applying a separation between data and command, see the Parameterized command execution implementation section.

    • Usually parameterized methods take a list as input (a command name and its arguments), not a string. So, if a method accepts a string, this method is most likely vulnerable to command injection.

  • If it is not possible to use APIs or parameterized methods, you can use environment variables to pass user-controlled data.

  • Do not use user-controlled data as a name when setting environment variables. Instead, generate random names.

Clarification

Environment variables can be used to inject commands and execute arbitrary code. You can find a list of dangerous environment variables at Application Security Cheat Sheet: Command Injection.

  • Implement comprehensive input validation, see the Input Validation page.

    • Define an allow list of commands.

    • Define an allow list of arguments.

    • Define an allow list of characters (alphanumeric characters only) to validate commands, arguments and operands.

    • Define an allow list of environment variable names.

    • Define an allow list of environment variable values.

    • Do not use block list validation.

  • Do not run commands inside of a shell.

    • Do not pass a shell to a parameterized method as a command. For example, the following snippet is vulnerable to command injection:

      // vulnerable to command injection
      system(['/bin/sh', '-c', user_input])
    • Do not enable running commands inside of a shell using an argument like the "shell" in child_process.Spawn in Node.js:

      const { spawn } = require('node:child_process');
      // command execution when shell is set to false
      const cmd = spawn('cmd', ['arg1', 'arg2']);
      // command execution when shell is set to true
      const cmd = spawn('cmd arg1 arg2', { shell: true });
  • Use -- to separate operands from arguments.

Clarification

Unfortunately, using parameterized functions for command execution does not protect from the argument injection attack. Depending on the command used, argument injection can provide an attacker with additional leverage, such as a primitive for writing arbitrary files or even executing arbitrary code.

You can find arguments for various commands that can be abused at Application Security Cheat Sheet: Command Injection - Argument Injection.

-- allows you to explicitly separate operands from arguments. For example, the code in the snippet below takes the data from user_input as an argument and the OS command will print the listing of the the current folder.

user_input = '-la'
system('ls', user_input)
// => ls -la

However, using -- can help avoid this issue. The code in the snippet below will return an error because -la will be treated as a name, not an argument.

user_input = '-la'
system('ls', '--', user_input)
// => ls -- -la
// ls: -la: No such file or directory
  • Be aware that methods not directly related to executing OS commands can execute OS commands under the hood and be vulnerable to command injection.

Clarification

For example, Kernel.open is used to open files in Ruby but can be used to execute OS commands:

open("| echo 'echo from open'").read()
# => 'echo from open\n

You can find a list of dangerous methods at Application Security Cheat Sheet: Command Injection.

  • Use a sandbox to execute OS commands.

Parameterized command execution implementation

Use the os/exec package to execute OS commands.

package main

import (
    "fmt"
    "os/exec"
)

func main() {
    cmd := exec.Command("cmd", "arg1", "arg2")
    out, _ := cmd.Output()
    fmt.Printf("%s", out)
}

References

Last updated