Why is this an issue?
Code injections occur when applications allow the dynamic execution of code instructions from untrusted data.
An attacker can influence the
behavior of the targeted application and modify it to get access to sensitive data.
What is the potential impact?
An attacker exploiting a dynamic code injection vulnerability will be able to execute arbitrary code in the context of the vulnerable
application.
The impact depends on the access control measures taken on the target system OS. In the worst-case scenario, the process that executes the code
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 code 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 arbitrary code execution because it dynamically runs JavaScript code built from untrusted data.
Noncompliant code example
function (req, res) {
let operation = req.query.operation
eval(`product_${operation}()`) // Noncompliant
res.send("OK")
}
Compliant solution
const allowed = ["add", "remove", "update"]
let operationId = req.query.operationId
const operation = allowed[operationId]
eval(`product_${operation}()`)
res.send("OK")
How does this work?
Allowing users to execute code dynamically generally creates more problems than it solves.
Anything that can be done via dynamic code execution can usually be done via a language’s native SDK and static code.
Therefore, our suggestion
is to avoid executing code dynamically.
If the application requires the execution of dynamic code, additional security measures must be taken.
Dynamic parameters
When the untrusted values are only expected to be values used in standard processing, it is generally possible to provide them as parameters of the
dynamic code. In that case, care should be taken to ensure that only the name of the untrusted parameter is passed to the dynamic
code and not that its value is expanded into it. After that, the dynamic code will be able to safely access the untrusted parameter content and
perform the processing.
Allow list
When the untrusted parameters are expected to contain operators, function names or other reflection-related values, best practices would encourage
using an allow list. This one would contain a list of accepted safe values that can be used as part of the dynamic code.
When receiving an untrusted parameter, the application would verify its value is contained in the configured allow list. If it is present, the
parameter is accepted. Otherwise, it is rejected and an error is raised.
Another similar approach is using a binding between identifiers and accepted values. That way, users are only allowed to provide identifiers, where
only valid ones can be converted to a safe value.
The example compliant code uses such a binding approach.
How to fix it in MongoDB
Code examples
The following code is vulnerable to arbitrary code execution because it builds a JavaScript code snippet from untrusted data. This piece of code is
then used as an operand for a dangerous evaluation MongoDB operator.
When performing a database query that way, the MongoDB server will interpret the JavaScript code as part of the data retrieval process. This might
lead to arbitrary code execution on the database server.
Noncompliant code example
const client = new MongoClient(mongoURI);
async function run() {
const database = client.db('example');
const products = database.collection('product');
const query = { $where: `isString(this.${req.query.prop})`};
const product = await products.findOne(query); // Noncompliant
}
run().catch(console.dir);
Compliant solution
const client = new MongoClient(mongoURI);
async function run() {
const allowed = ["name", "price", "stock"]
let propId = req.query.propId
const database = client.db('example');
const products = database.collection('product');
const query = { $where: `isString(this.${allowed[propId]})`};
const product = await products.findOne(query); // Noncompliant
}
run().catch(console.dir);
How does this work?
MongoDB provides JavaScript code-aware search operators to allow developers to perform advanced queries. In most cases, the same result can be
achieved by combining classical, safer, operators, or by performing additional data filtering on the application side.
Note that MongoDB servers implement a sandboxed JavaScript engine to run the operator-related code. Without other vulnerabilities in the database
engine, the execution of arbitrary code is thus not possible through the exploitation of evaluation operators. However, a successful injection could
still lead to the compromise of the MongoDB server hosted data. Moreover, the discovery by an attacker of a way to escape the sandbox should also be
considered.
Safer operators
Wherever possible, safe operators that do not evaluate JavaScript expressions should be preferred over dangerous ones. Dangerous operators include:
* $where
* $accumulator
* $function
If the application requires a complex logic to properly filter a query’s results, it might be considered to implement it on the application side
rather than in the database itself. That way, only simple operators can be used in the database query.
Allow list
When the untrusted parameters are expected to contain operators, function names or other reflection-related values, best practices would encourage
using an allow list. This one would contain a list of accepted safe values that can be used as part of the dynamic code.
When receiving an untrusted parameter, the application would verify its value is contained in the configured allow list. If it is present, the
parameter is accepted. Otherwise, it is rejected and an error is raised.
Another similar approach is using a binding between identifiers and accepted values. That way, users are only allowed to provide identifiers, where
only valid ones can be converted to a safe value.
The example compliant code uses such a binding approach.
Resources
Articles & blog posts
Standards