Skip to main content

    Command injection prevention for Python

    This is a command injection prevention cheat sheet by Semgrep, Inc. It contains code patterns of potential ways to run an OS command in an application. Instead of scrutinizing code for exploitable vulnerabilities, the recommendations in this cheat sheet pave a safe road for developers that mitigate the possibility of command injection in your code. By following these recommendations, you can be reasonably sure your code is free of command injection.

    Check your project using Semgrep

    The following command runs an optimized set of rules for your project:

    semgrep --config p/default

    1. Running an OS command

    1.A. Using the subprocess module

    The subprocess module allows you to start new processes, connect to their input/output/error pipes, and obtain their return codes. Methods such as Popen, run, call, check_call, check_output are intended for running commands provided as an argument. Allowing user input in a command that is passed as an argument to one of these methods can create an opportunity for a command injection vulnerability.

    Example:

    import subprocess
    import sys

    # Vulnerable
    user_input = "foo && cat /etc/passwd" # value supplied by user
    subprocess.call("grep -R {} .".format(user_input), shell=True)

    # Vulnerable
    user_input = "cat /etc/passwd" # value supplied by user
    subprocess.run(["bash", "-c", user_input], shell=True)

    # Not vulnerable
    user_input = "cat /etc/passwd" # value supplied by user
    subprocess.Popen(['ls', '-l', user_input])

    # Not vulnerable
    subprocess.check_output('ls -l dir/')

    References

    Mitigation

    Do not let a user input into subprocess methods. Alternatively:

    • Always try to use internal Python API (if it exists) instead of running an OS command. In other words, use internal language features instead of invoking commands that can be exploited.
    • Don't pass user-controlled input.
    • If it is not possible, use an array with a sequence of program arguments instead of a single string.
    • Use shlex.split to correctly parse a command string into an array and shlex.quote to correctly sanitize input as a command-line parameter.
    • Do not include command arguments in a command string, use parameterization instead. For example:
      Use:
      subprocess.run(["/path/to/myCommand", "myArg1", input_value])
      Instead of:
      subprocess.run(["bash", "-c", "myCommand myArg1 " + input_value])
    • If it is not possible, strip everything except alphanumeric characters from an input provided for the command string and arguments.

    Semgrep rule

    python.lang.security.audit.dangerous-subprocess-use

    1.B. shell=True

    Functions from the subprocess module have the shell argument for specifying if the command should be executed through the shell. Using shell=True is dangerous because it propagates current shell settings and variables. This means that variables, glob patterns, and other special shell features in the command string are processed before the command is run, making it much easier for a malicious actor to execute commands. The subprocess module allows you to start new processes, connect to their input/output/error pipes, and obtain their return codes. Methods such as Popen, run, call, check_call, check_output are intended for running commands provided as an argument ('args'). Allowing user input in a command that is passed as an argument to one of these methods can create an opportunity for a command injection vulnerability.

    Example:

    # prints home directory
    subprocess.call('echo $HOME', shell=True)

    # throws an error
    subprocess.call('echo $HOME', shell=False)

    References

    Mitigation

    Avoid using shell=True. Alternatively, use shell=False instead.

    Semgrep rule

    python.lang.security.audit.subprocess-shell-true

    1.C. Using os module to execute commands

    The os module provides a portable way of using operating system dependent functionality. Methods such as system, popen and deprecated popen2, popen3 and popen4 are intended for running commands provided as a string. Letting user supplied data into a command that is passed as an argument to one of these methods can create an opportunity for a command injection vulnerability.

    Example:

    import os

    # Vulnerable
    user_input = "foo && cat /etc/passwd" # value supplied by user
    os.system("grep -R {} .".format(user_input))

    # Vulnerable
    user_input = "foo && cat /etc/passwd" # value supplied by user
    os.popen("ls -l " + user_input)

    References

    Mitigation

    Do not let a user input into os methods. Alternatively:

    • Always try to use an internal Python API (if it exists) instead of running an OS command.
    • Consider using subprocess functions with array of program arguments.
    • Don’t pass user-controlled input.
    • If it is not possible, then don’t let running arbitrary commands. Use an allowlist for inputs.

    Semgrep rule

    python.lang.security.audit.dangerous-system-call

    1.D. Using os module to spawn a process

    The os module allows executing the program path in a new process. Variations of spawn method including spawnl, spawnle, spawnlp, spawnlpe, spawnv, spawnve, spawnvp, spawnvpe, posix_spawn and posix_spawnp are intended for spawning a process with a program passed as a string argument. Allowing spawning of arbitrary programs or running shell processes with arbitrary arguments may result in a command injection vulnerability.

    Example:

    import os

    # Vulnerable
    user_input = "/foo/bar" # value supplied by user
    os.spawnlpe(os.P_WAIT, user_input, ["-a"], os.environ)

    # Vulnerable
    user_input = "cat /etc/passwd" # value supplied by user
    os.spawnve(os.P_WAIT, "/bin/bash", ["-c", user_input], os.environ)

    References

    Mitigation

    Do not let a user input into spawn methods. Alternatively:

    • Always try to use an internal Python API (if it exists) instead of running an OS command.
    • Don’t pass user-controlled input.
    • Use shlex.split to correctly parse a command string into an array and shlex.quote to correctly sanitize input as a command-line parameter.
    • Do not include command arguments in a command string, use parameterization instead. For example:
      Use:
      os.spawnve(os.P_WAIT, "/path/to/myCommand", ["myArg1", input_value], os.environ)
      Instead of:
      os.spawnve(os.P_WAIT, "/bin/bash", ["-c", "myCommand myArg1 " + input_value], os.environ)
    • If it’s not possible to avoid, strip everything except alphanumeric characters from an input provided for the command string and arguments.

    Semgrep rule

    python.lang.security.audit.dangerous-spawn-process.dangerous-spawn-process

    1.E. Replacing current process with exec

    Execution methods of the os module are intended to execute a new program, replacing the current process. The available methods are execl, execle, execlp, execlpe, execv, execve, execvp, and execvpe. Allowing running of arbitrary programs or running shell processes with arbitrary arguments may result in a command injection vulnerability.

    Example:

    import os

    # Vulnerable
    user_input = "/evil/code" # value supplied by user
    os.execl(user_input, '/foo/bar', '--do-smth')

    # Vulnerable
    user_input = "cat /etc/passwd" # value supplied by user
    os.execve("/bin/bash", ["/bin/bash", "-c", user_input], os.environ)

    References

    Mitigation

    Do not let a user input into exec methods. Alternatively:

    • Always try to use internal an Python API (if it exists) instead of running an OS command.
    • Don't pass user-controlled input.
    • Use shlex.split to correctly parse a command string into an array and shlex.quote to correctly sanitize input as a command-line parameter.
    • Do not include command arguments in a command string, use parameterization instead. For example:
      Use:
      os.execve("/path/to/myCommand", ["myCommand", "myArg1", input_value], os.environ)
      Instead of:
      os.execve("/bin/bash", ["/bin/bash", "-c", "myCommand myArg1 " + input_value], os.environ)
    • If it's not possible to avoid, strip everything except alphanumeric characters from an input provided for the command string and arguments.

    Semgrep rule

    python.lang.security.audit.dangerous-spawn-process.dangerous-spawn-process

    1.F. Wildcard character in a system call that spawns a shell

    Spawning a shell or executing a Unix shell command with a wildcard leads to normal shell expansion, which can have unintended consequences if there exist any non-standard file names. Consider a file named '-e sh script.sh' -- this will execute a script when 'rsync' is called.

    Example:

    # directory example
    [root@user public]# ls -al
    total 20
    drwxrwxr-x. 5 user user 4096 Oct 28 17:04 .
    drwx------. 22 user user 4096 Oct 28 16:15 ..
    drwxrwxr-x. 2 user user 4096 Oct 28 17:04 DIR1
    drwxrwxr-x. 2 user user 4096 Oct 28 17:04 DIR2
    drwxrwxr-x. 2 user user 4096 Oct 28 17:04 DIR3
    -rw-rw-r--. 1 user user 0 Oct 28 17:03 file1.txt
    -rw-rw-r--. 1 nobody nobody 0 Oct 28 16:38 -rf


    # running Python code like this will use `-rf` as an argument for rm and force delete all directories
    os.system("/bin/rm *")

    References

    Mitigation

    Avoid wildcards in Unix shell commands.

    Semgrep rule

    python.lang.security.audit.system-wildcard-detected

    1.G. Running shell commands asynchronously

    The asyncio.subprocess is an async or await API to create and manage subprocesses. Such methods as create_subprocess_shell and Event Loop's subprocess_shell are intended for running shell commands provided as an argument 'cmd'. Allowing user input into a command that is passed as an argument to one of these methods can create an opportunity for a command injection vulnerability.

    Example:

    import asyncio

    # Vulnerable
    user_input = "cat /etc/passwd" # value supplied by user
    loop = asyncio.new_event_loop()
    # This is similar to the standard library subprocess.Popen class called with shell=True
    loop.subprocess_shell(asyncio.SubprocessProtocol, user_input)

    # Vulnerable
    user_input = "cat /etc/passwd" # value supplied by user
    asyncio.subprocess.create_subprocess_shell(user_input)

    References

    Mitigation

    Do not let a user input into asyncio.subprocess methods. Alternatively:

    • Always try to use an internal Python API (if it exists) instead of running an OS command.
    • Consider using asyncio.subprocess functions with array of program arguments (for example: create_subprocess_exec).
    • Don't pass user-controlled input.
    • If it's not possible, then don't let running arbitrary commands. Use an allowlist for inputs.

    Semgrep rule

    python.lang.security.audit.dangerous-asyncio-shell.dangerous-asyncio-shell

    1.H. Creating subprocesses asynchronously

    The asyncio.subprocess also allows asynchronous creation of subprocesses. Such methods as create_subprocess_exec and Event Loop's subprocess_exec are intended for creating a subprocess from one or more string arguments specified by args. Allowing user input into a command that is passed as an argument to one of these methods can create an opportunity for a command injection vulnerability.

    Example:

    import asyncio

    # Vulnerable
    user_input = "/evil/exe" # value supplied by user
    loop = asyncio.new_event_loop()
    loop.subprocess_exec(asyncio.SubprocessProtocol, [user_input, "--parameter"])

    # Vulnerable
    user_input = "cat /etc/passwd" # value supplied by user
    asyncio.subprocess.create_subprocess_exec("bash", ["bash", "-c", user_input])

    # Not vulnerable
    user_input = "/evil/exe" # value supplied by user
    loop = asyncio.new_event_loop()
    loop.subprocess_exec(asyncio.SubprocessProtocol, ['ls', '-l', user_input])

    References

    Mitigation

    Do not let a user input into asyncio subprocess methods. Alternatively:

    • Always try to use an internal Python API (if it exists) instead of running an OS command.
    • Don't pass user-controlled input.
    • Use shlex.split to correctly parse a command string into an array and shlex.quote to correctly sanitize input as a command-line parameter.
    • Do not include command arguments in a command string, use parameterization instead. For example:
      Use:
      asyncio.subprocess.create_subprocess_exec("/path/to/myCommand", ["myCommand", "myArg1", input_value])
      Instead of:
      asyncio.subprocess.create_subprocess_exec("bash", ["bash", "-c", "myCommand myArg1 " + input_value])
    • If it's not possible to avoid, strip everything except alphanumeric characters from an input provided for the command string and arguments.

    Semgrep rule

    python.lang.security.audit.dangerous-asyncio-exec.dangerous-asyncio-exec
    python.lang.security.audit.dangerous-asyncio-create-exec.dangerous-asyncio-create-exec

    Not finding what you need in this doc? Ask questions in our Community Slack group, or see Support for other ways to get help.