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 Core PHP
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.
Noncompliant code example
$command = $_GET['cmd'];
exec($command, $output, $ret); // Noncompliant
echo ($ret == 0 ? "OK" : "KO");
Compliant solution
$allowedCommands = [["/bin/ping","-c","1","--"],["/usr/bin/host","--"]];
$cmd = $allowedCommands[$_GET["cmdId"]];
$cmd[] = $_GET["host"];
$process = proc_open($cmd, [], $pipes);
$ret = proc_close($process);
echo ($ret == 0 ? "OK" : "KO");
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.
In the example compliant code, a static list of allowed commands is used. Users are only allowed to provide a command index that will be used to
access this list. The command resulting from the list access can be considered trusted.
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 proc_open
.
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 proc_open
function is used in place of the less safe exec
alternative. Moreover, the
command
parameter of this function is set to an array. That way, the function will properly escape all the array elements and concatenate
them 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 proc_open
function with an array of arguments as a parameter disables shell integration.
How to fix it in Symfony
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.
Noncompliant code example
public function unsafe(Request $request): JsonResponse
{
$process = Process::fromShellCommandline($request->query->get("cmd")); // Noncompliant
$process->run();
$code = $process->wait();
return $this->json($code == 0);
}
Compliant solution
public function safe(Request $request): JsonResponse
{
$allowedCommands = [["/bin/ping","-c","1","--"],["/usr/bin/host","--"]];
$cmd = $allowedCommands[$request->query->get("cmdId")];
$cmd[] = $request->query->get("host");
$process = new Process($cmd);
$process->run();
$code = $process->wait();
return $this->json($code == 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.
In the example compliant code, a static list of allowed commands is used. Users are only allowed to provide a command index that will be used to
access this list. The command resulting from the list access can be considered trusted.
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 Symfony\Component\Process\Process
.
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 Process
constructor is used in place of the less safe fromShellCommandline
function.
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 Process
constructor is preferred over the less safe fromShellCommandline
Process
factory. This way of creating Process
instances disables shell integration by default.
Resources
Documentation
Standards