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 Node.js
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
const { execSync } = require('child_process')
cmd = req.query.cmd
execSync(cmd) // Noncompliant
Compliant solution
const { spawnSync } = require('child_process')
const cmdId = parseInt(req.query.cmdId)
let host = req.query.host
host = typeof host === "string"? host : "example.org"
const allowedCommands = [
{exe:"/bin/ping", args:["-c","1","--"]},
{exe:"/bin/host", args:["--"]}
]
const cmd = allowedCommands[cmdId]
spawnSync(cmd.exe, cmd.args.concat(host))
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 trusted commands is used. Users are only allowed to submit an index in this array in place of a
full command name.
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 child_process.spawn
.
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 spawn
function from child_process
is used in place of its less secure
exec
counterpart. It accepts command arguments as an array and performs a proper escaping of its element before building the command line
to run.
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.
The spawn
function that is used in the example compliant code disables shell integration by default.
Pitfalls
Loose typing
Because JavaScript is a loosely typed language, extra care should be taken when accepting user-controlled parameters. Indeed, some methods, that
can be used to sanitize untrusted parameters, sometimes accept both objects and object arrays.
For example, the Array.concat
function accepts an array as argument and will append all of its elements to its target. When an
untrusted parameter is an array, while a single string was expected, using concat
to build a command argument list can result in an
arbitrary argument injection.
It is therefore of prime importance to check the type of untrusted parameters before processing them.
In the above compliant code example, the ambiguous concat
function is used. However, a type check has been introduced to prevent any
unexpected issue.
How to fix it in SSH2
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
const { Client } = require('ssh2')
const conn = new Client()
conn.on('ready', () => {
conn.exec(req.query.cmd, (err, stream) => {}) // Noncompliant
})
conn.connect({
host: 'example.org',
username: 'user',
password: 'password'
})
Compliant solution
const { Client } = require('ssh2')
const shell = require('shell-escape-tag')
const cmdId = parseInt(req.query.cmdId)
let host = req.query.host
host = typeof host === "string"? host : "example.org"
host = shell.escape(host)
const allowedCommands = ["/bin/ping -c 1 --", "/bin/host --"]
const cmd = `${allowedCommands[cmdId]} ${host}`
const conn = new Client()
conn.on('ready', () => {
conn.exec(cmd, (err, stream) => {})
})
conn.connect({
host: 'example.org',
username: 'user',
password: 'password'
})
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 trusted commands is used. Users are only allowed to submit an index in this array in place of a
full command name.
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 shell-escape-tag
.
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 escape
function from the shell-escape-tag
is used to properly escape the user-supplied
command argument. It can then safely be used in the trusted command template.
Pitfalls
Loose typing
Because JavaScript is a loosely typed language, extra care should be taken when accepting user-controlled parameters. Indeed, some methods, that
can be used to sanitize untrusted parameters, sometimes accept both objects and object arrays.
For example, the Array.concat
function accepts an array as argument and will append all of its elements to its target. When an
untrusted parameter is an array, while a single string was expected, using concat
to build a command argument list can result in an
arbitrary argument injection.
It is therefore of prime importance to check the type of untrusted parameters before processing them.
In the above compliant code example, the shell.escape
function has this behavior. However, a type check has been introduced to prevent
any unexpected issue.
Resources
Documentation
Standards