Why is this an issue?
OS command injections occur when applications build command lines from untrusted data before executing them with a system shell.
In that case,
an attacker can tamper with the command line construction and force the execution of unexpected commands. This can lead to the compromise of the
underlying operating system.
What is the potential impact?
An attacker exploiting an OS command injection vulnerability will be able to execute arbitrary commands on the underlying operating system.
The impact depends on the access control measures taken on the target system OS. In the worst-case scenario, the process runs with root privileges,
and therefore any OS commands or programs may be affected.
Below are some real-world scenarios that illustrate some impacts of an attacker exploiting the vulnerability.
Denial of service and data leaks
In this scenario, the attack aims to disrupt the organization’s activities and profit from data leaks.
An attacker could, for example:
- download the internal server’s data, most likely to sell it
- modify data, send malware
- stop services or exhaust resources (with fork bombs for example)
This threat is particularly insidious if the attacked organization does not maintain a disaster recovery plan (DRP).
Root privilege escalation and pivot
In this scenario, the attacker can do everything described in the previous section. The difference is that the attacker also manages to elevate
their privileges to an administrative level and attacks other servers.
Here, the impact depends on how much the target company focuses on its Defense In Depth. For example, the entire infrastructure can be compromised
by a combination of OS injections and misconfiguration of:
- Docker or Kubernetes clusters
- cloud services
- network firewalls and routing
- OS access control
How to fix it in Python Standard Library
Code examples
The following code is vulnerable to command injections because it is using untrusted inputs to set up a new process. Therefore an attacker can
execute an arbitrary program that is installed on the system.
Especially, in this example, if the host request parameter contains system shell control characters, the expected
ping
command behavior will be changed.
Noncompliant code example
def ping():
cmd = "ping -c 1 %s" % request.args.get("host", "www.google.com")
status = os.system(cmd) # Noncompliant
return str(status == 0)
Compliant solution
def safe_ping():
host = request.args.get("host", "www.google.com")
status = subprocess.run(["ping", "-c", "1", "--", host]).returncode
return str(status == 0)
How does this work?
Allowing users to execute operating system commands generally creates more problems than it solves.
Anything that can be done via operating system commands can usually be done via a language’s native SDK.
Therefore, our first suggestion is to
avoid using OS commands in the first place.
However, if the application requires running OS commands with user-controlled data, here are some
security suggestions.
Pre-Approved commands
If the application aims to execute only a small number of OS commands (for example, ls
, pwd
, and grep
), the
cleanest way to avoid this problem is to validate the input before using it in an OS command.
Create a list of authorized and secure commands that you want the application to be able to execute. Use absolute paths to avoid any ambiguity.
If a user input does not match an entry in this list, it should be rejected because it is considered unsafe.
Depending on the number of commands you want the application to support, the list can be either a regex string or any array type. If you use
regexes, choose simple regexes to avoid ReDOS attacks. For example, you can accept only a specific set of executables, by using
^/bin/(ls|pwd|grep)$
.
Important note: The application must do validation on the server side. Not on client-side front-ends.
Neutralize special characters
If the application is to execute complex commands that cannot be controlled thanks to pre-approved lists, the cleanest approach is to use special
sanitization components, such as subprocess
.
The library helps you to get rid of common dangerous characters, such as:
If user input is to be included in the arguments of a command, the application must ensure that dangerous options or argument delimiters are
neutralized.
Argument delimiters count '
, -
and spaces.
For example, the find
command from UNIX supports the dangerous argument -exec
.
In this case, option processing can be
terminated with a string containing --
or with special options. For example, git
supports --end-of-options
since its version 2.24.
In the example compliant code, using the subprocess.run
function helps to escape the passed arguments. It accepts a list of command
arguments that will be properly escaped and concatenated to form the command line to execute.
Disable shell integration
In most cases, command execution libraries propose two ways to execute external program: with or without shell integration.
When shell integration is allowed, an attacker with control over the command arguments can simply execute additional external programs using system
shell features. For example, on Unix, command pipelining (|
) or string interpolation ($()
, <()
, etc.) can be
used to break out of a command call.
Therefore, it is generally preferable to disable shell integration.
In the example compliant code, using the subprocess
module’s functions is preferred over older alternative as the os
or
popen
modules. Indeed, subprocess
, while still a dangerous library, disables the system shell’s syntax interpretation by
default.
How to fix it in Paramiko
Code examples
The following code is vulnerable to command injections because it is using untrusted inputs to set up a new process. Therefore an attacker can
execute an arbitrary program that is installed on the system.
In the following example, if the host request parameter contains system shell control characters, the expected ping
command behavior will be changed.
Noncompliant code example
client = SSHClient()
client.connect("example.org", username=USER, password=PASS)
client.exec_command(request.args.get("cmd")) # Noncompliant
Compliant solution
client = SSHClient()
client.connect("example.org", username=USER, password=PASS)
DIAG_CMD=["/bin/ping -c 1 -- %s", "/bin/host -- %s"]
cmd = DIAG_CMD[int(request.args.get('cmdId'))]
cmd = cmd % shlex.quote(request.args.get('host'))
client.exec_command(cmd)
How does this work?
Allowing users to execute operating system commands generally creates more problems than it solves.
Anything that can be done via operating system commands can usually be done via a language’s native SDK.
Therefore, our first suggestion is to
avoid using OS commands in the first place.
However, if the application requires running OS commands with user-controlled data, here are some
security suggestions.
Pre-Approved commands
If the application aims to execute only a small number of OS commands (for example, ls
, pwd
, and grep
), the
cleanest way to avoid this problem is to validate the input before using it in an OS command.
Create a list of authorized and secure commands that you want the application to be able to execute. Use absolute paths to avoid any ambiguity.
If a user input does not match an entry in this list, it should be rejected because it is considered unsafe.
Depending on the number of commands you want the application to support, the list can be either a regex string or any array type. If you use
regexes, choose simple regexes to avoid ReDOS attacks. For example, you can accept only a specific set of executables, by using
^/bin/(ls|pwd|grep)$
.
Important note: The application must do validation on the server side. Not on client-side front-ends.
Neutralize special characters
If the application is to execute complex commands that cannot be controlled thanks to pre-approved lists, the cleanest approach is to use special
sanitization components, such as shlex
.
The library helps you to get rid of common dangerous characters, such as:
If user input is to be included in the arguments of a command, the application must ensure that dangerous options or argument delimiters are
neutralized.
Argument delimiters count '
, -
and spaces.
For example, the find
command from UNIX supports the dangerous argument -exec
.
In this case, option processing can be
terminated with a string containing --
or with special options. For example, git
supports --end-of-options
since its version 2.24.
In the example compliant code, the quote
function from the shlex
is used to encode the user-controlled command argument.
It escapes argument delimiters and shell control characters.
Resources
Documentation
Standards