Server Side JavaScript Injection
Published on June 2, 2020 by Rodrigo Fonseca
The Gist of It
Let’s first start by explaining some core concepts necessary to understand the security implications of Server Side JavaScript Injection:
JavaScript - JavaScript is a programming language on its own right. However, it is normally associated to WWW and HTML, as JavaScript engines were initially implemented on web browsers. The main objective was to interact at client-side, once the HTML code was loaded on the browser. Therefore, it would allow to programmatically modify the code/properties of the HTML page and in turn make the page interact with the user. A wide set of frameworks such as Jquery are built on top of JavaScript to enhance user experience when browsing web pages.
Server - Server side components are responsible for dynamically constructing the page upon request, based on the data / parameters sent by the user. Whereas client-side scripting is normally running on a sand-boxed environment (to isolate the execution of the pages loaded on the browser, and avoid interaction with the underlying operating system) and under origin rules (to avoid sending/receiving data from other domains rather than the source), server-side JavaScript is not bound to such restrictions.
With these two concepts explained we can now explain the name and subsequently explain the vulnerability itself.
The most common occurrence of JavaScript injection is located on the client side: Cross Site Scripting https://en.wikipedia.org/wiki/Cross-site_scripting.
Cross Site Scripting is the ability of an attacker to inject code (JavaScript) into another user’s browser, and therefore potentially interact with the victim browser content to gain unauthorised access to the user context.
With the appearance of NodeJS, JavaScript gained very significant popularity as a server-side, back-end powering language. This turned some client slide data injection vulnerabilities into server side vulnerabilities such as XSS into Server Side JavaScript Injection.
Server Side JavaScript injection is the ability for a user to inject code which will in turn be evaluated by the server, and therefore would allow an attacker to potentially execute arbitrary code under the context of the server and interaction with the filesystem, which may lead to the full compromise of the host.
Denial of Service conditions are also possible, by killing the process or entering an infinite loop as NodeJS runs in a single thread.
How?
The vulnerability itself happens due to user controlled unsanitized data being passed to functions that can evaluate JavaScript code such as eval().
An example of this would be:
// Handle Author update on POST
exports.author_update_post = [
// Validate fields.
body('first_name').isLength({ min: 1 }).trim().withMessage('First name must be specified.
.isAlphanumeric().withMessage('First name has non-alphanumeric characters.'),
body('family_name').isLength({ min: 1 }).trim().withMessage('Family name must be specifie
.isAlphanumeric().withMessage('Family name has non-alphanumeric characters.'),
body('date_of_birth', 'Invalid date of birth').optional({ checkFalsy: true }).isISO8601()
body('date_of_death', 'Invalid date of death').optional({ checkFalsy: true }).isISO8601()
// Sanitize fields.
sanitizeBody('first_name').escape(),
sanitizeBody('family_name').escape(),
sanitizeBody('date_of_birth').toDate(),
sanitizeBody('date_of_death').toDate(),
// Process request after validation and sanitization.
(req, res, next) => {
// Extract the validation errors from a request.
const errors = validationResult(req);
// Create Author object with escaped and trimmed data
var author = new Author(
{
first_name: req.body.first_name,
family_name: req.body.family_name,
date_of_birth: req.body.date_of_birth,
date_of_death: req.body.date_of_death,
characteristics: eval("(" + req.body.characteristics + ")") <==========
}
);
if (!errors.isEmpty()) {
// There are errors. Render form again with sanitized values/errors messages.
res.render('author_form', {title: 'Create Author', author: author, errors: errors.array() });
return;
}
else {
// Data from form is valid.
// Save author
author.save(function (err) {
if (err) {return next(err); }
// Successful - redirect to new author record.
res.redirect(author.url);
});
}
}
]
This excerpt of code is taken from Mozilla Node.JS learning material with a slight modification to showcase the vulnerability. In the code above we can see the usage of eval() on the creation of a new Author.
Its intended use was to parse the parameter characteristics that through normal usage would contain a JSON object. In contrast with the other parameters, characteristics isn’t validated nor sanitized. This opens the possibility of an attacker sending malicious JavaScript content instead of a JSON object which will be eval()uated by the server.
This kind of vulnerability may be introduced due to lack of knowledge of the security implications of some JavaScript functions or by a lack of an effective secure development lifecycle, where production code is released without the appropriate due diligence processes in place.
It should be kept in mind that eval() is not the only function vulnerable to this. A list of vulnerable functions and how they are vulnerable was created for this article and follows underneath.
Stand-Alone Functions
eval()
eval("<JavaScript Code>")
eval() takes only one parameter and interprets it as JavaScript code.
Function()
var f = new Function("<JavaScript Code>");
f();
Function() like eval() takes one parameter and interprets it as JavaScript code. Contrary to eval() it doesn’t run the code when it is inserted, but only when it is called, like in the example with f().
child_process Dependent Functions
The following functions come from the dependency “child_process”. They execute operating system commands instead of JavaScript code. They are still referred here due to the vulnerability being very similar, sharing the same environment (Node.JS).
exec()
var child_process = require('child_process')
child_process.exec("<shell commands>");
With exec(), spawns a new shell where argument passed to the function is executed.
execFile()
var child_process = require('child_process')
child_process.execFile("<shell commands>", {shell:true});
child_process.execFile("/path/to/malicious/file");
execFile() can be abused in two ways. The first one is when the option shell is enabled which let’s the first argument to be shell commands and works like exec(). The second way, when the option shell is not enabled requires the attacker to store the payload on the server and know its location. If those two requirements are met, the attacker can then run the malicious payload by passing the file path.
spawn()
var child_process = require('child_process')
child_process.spawn("<shell commands>", {shell:true});
child_process.spawn("/path/to/malicious/file", ['shell', 'args']);
spawn() can be abused in 2 ways. The first one with the option shell enabled which let’s any arbitrary shell command to be run like exec() The second way in case shell is not enabled 2 entry points are possible. Either through the file path. Where it would work the same way and have the same requirements as execFile() or if the user doesn’t have control over the first argument but controls the array of arguments, depending on what program is being executed it would still be possible to exploit to run arbitrary shell commands.
fork()
var child_process = require('child_process')
child_process.fork("malicious.js");
fork() is the hardest one to exploit as it has some limitations. It requires a malicious file to be stored on the victim server and know its location, but it also requires the file to be a valid JavaScript file.
Mitigations
The most effective mitigation would consist on avoiding the aforementioned functions as well as a through understanding of the code base of third party modules. For example, in the snippet of code shown above to illustrate a scenario of eval() being vulnerable, the same goal could be accomplished through the usage of JSON.parse() and at the same time mitigate the risk.
With that being said, there are scenarios where it is not only possible to avoid the vulnerable functions but it’s also required to pass user input to it. In these scenarios the best approach is through the validation and sanitization of the input.
Validation of the input can be done through already standardized functions or through White-List RegEx where only certain characters or a certain format is allowed.
Sanitization can be done by escaping any characters that can be interpreted by the vulnerable functions. Most frameworks already have functions to safely sanitize the input of users.
You may also be interested in...
Using msbuild to bypass application white-listing Is a well known and documented techique. You simply need to add some C# code to a msbuild task within an msbuild XML project file and msbuild will happily compile and run your code.
See more
Social media information leakage is a valuable asset for attackers.
See more