Assessing Emerging JavaScript Platforms with Express.js and Node.js – What to Look For
Overview
Node.js is known as one of the most important emerging technologies. It is an event driven open source runtime to create server side applications. It is a highly customizable server engine that is popular amongst JavaScript coders to create real time web APIs. It processes in a loop and sets up to respond to the requests.
Changing the paradigm, the JavaScript code runs in the back-end, outside a browser. Applications are single threaded; therefore it’s necessary to build or write a Web server besides the application logic.
In order to avoid writing the Web Server code, this emerging technology is Express.js, or also known simply as Express. Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
Given the novelty of this technology, several security aspects are missing or misunderstood. Therefore, VerSprite tried to include common issues related to Node.js and Express, which could apply to other new technologies as well.
The problems to be addressed were documented and tested. It is important to use a method to test all known vulnerabilities and document all the security test activities.
Global Namespace Pollution
JavaScript has two scopes: global and local. A variable that is declared outside a function definition is a global variable, and its value is accessible and modifiable throughout the program. A variable that is declared inside a function definition is local. It is created and destroyed every time the function is executed, and it cannot be accessed by any code outside the function.
In node.js:
- variables by default have an implied global scope
- functions by default have an implied global scope
- all objects inherit from the native / built-in global objects
These properties could mean a change for most developers, if misunderstood or with limited knowledge of this inherent property, writing secure NodeJS web apps will present a challenge.
Take the following snippet of code for instance:
Global Variables Node.js Gotchas
In relevance to this code, each request will increase the global variable gbl by 1, as seen in the screenshot below for two different requests. In a PHP script, such a model would only show 1 for every request.
Global counter raised to ‘4’
Global counter raised to ‘5’
Depending on the context and sensitivity of a global variable or function, an attacker could exploit this behavior to her benefit to achieve desired effects. What could those be:
- As a web user, could bypass logic flows
- A malicious library could over-ride native, built-in or known objects, variables, functions to adversely impact sensitive code base/libraries
- In a shared coding environment, an inexperienced developer could unintentionally over-ride native, built-in or known objects, variables, functions – adversely impacting sensitive code base/libraries
HTTP Parameter Pollution (HPP)
The OWASP Testing Guide v4 defines HTTP Parameter Pollution as follows:
“Supplying multiple HTTP parameters with the same name may cause an application to interpret values in unanticipated ways. By exploiting these effects, an attacker may be able to bypass input validation, trigger application errors or modify internal variables values. As HTTP Parameter Pollution (in short HPP) affects a building block of all web technologies, server and client side attacks exist.”
Take the following snippet of code for instance:
*************************************************************************** var express = require('express'); var app = express(); app.get('/',function(req, res){ res.send('id: ' + req.query.id); console.log(“GET / id=”+req.query.id); }); app.listen(8080) ***************************************************************************
In this case when multiple parameters with the same name are sent, Express.js process them using a comma (,) as a delimiter. Note that depending on the execution of the retrieved parameters, it might cause the application to crash or present unexpected behaviors which would allow an attacker to achieve desired effects or circumvent the application’s logic. As an example of the above, using that same code snippet, we can observe a Parameter Pollution Test as follows: <img src=”/wp-content/uploads/2015/04/04272015-e.png” alt=”Passing ‘xyz’ as value” /> Passing ‘xyz’ as value <img src=”/wp-content/uploads/2015/04/04272015-f.png” alt=”Passing ‘xyz’ and ‘VerSprite’ as value” /> </br>Passing ‘xyz’ and ‘VerSprite’ as value <h4 id=”overview”>eval()</h4> The eval function (and its relatives, Function, setTimeout, and setInterval) provide access to the JavaScript compiler. This is sometimes necessary, but in most cases it indicates the presence of extremely bad coding. The eval function is known to be the most misused feature of JavaScript. Node.js just like every other high level language, carries over the known dangerous JavaScript eval() that can be trivially exploited to do server side injection (that were earlier only client side exploits like XSS). The following snippet of code shows the existence of this vulnerability by evaluating the ‘name’ parameter passed through the URL query string.
var express = require('express'); var app = express(); app.get('/',function(req, res){ var resp = eval(“(” + req.query.name + ”)”); res.send(‘Response</br>’+resp); }); app.listen(8080)
It also subsequently calls getDeviceId() on the TelephonyManager object
onCreate-BB@0x64 : 26 (00000064) iget-object v0, v4, Lcom/xinghai/contact/service/GogleService;->manager Landroid/telephony/TelephonyManager; 27 (00000068) invoke-virtual v0, Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String; 28 (0000006e) move-result-object v0 29 (00000070) iput-object v0, v4, Lcom/xinghai/contact/service/GogleService;->number Ljava/lang/String; [ onCreate-BB@0x74 ]
When accessing this application, we can observe an ‘undefined’ string in the response, given that no arguments were assigned, as shown below.
Accessing the vulnerable application.
As an exploitation example when we pass to the variable ‘name’ the value ‘process.kill()’, through the URL query string, the current process will be terminated by the attacker, thus disrupting the service’s availability.
Terminating the server’s process.
In case an attacker would want to execute arbitrary code in the affected application, open-source tools are available such as the following Node.js shell: https://github.com/evilpacket/node-shells
A tool like the above would generate a payload to be passed through the vulnerable input, containing a shell code and the attacker’s IP address and port. After the payload gets executed by the eval() function, the remote server will connect to attacker machine were she will be listening for new connections.
OS Command Execution
Command injection is an attack in which the goal is execution of arbitrary commands on the host operating system via a vulnerable application. Command injection attacks are possible when an application passes unsafe user supplied data (forms, cookies, HTTP headers etc.) to a system shell. In this attack, the attacker-supplied operating system commands are usually executed with the privileges of the vulnerable application. Command injection attacks are possible largely due to insufficient input validation.
Node.js can be vulnerable to Command Injections attacks just like any other programming language. The following snippet of code presents this scenario by using an application meant to ping other hosts, further we’ll observe how to exploit this attack by injecting other commands through the input.
var http = require('http'); var url = require(‘url’); var exe = require(‘child_process’); http.createServer(function(request, response) { var parsedUrl = url.parse(request.url, true); response.writeHead(200, {“Content-Type”: “text/html”}); exe.exec(‘ping –c 2 ’ + parsedUrl.query.ping, function(err, data) { response.write(“Hello ” + data); response.end(); }); }).listen(8080);
When we request this resource, we will observe a ‘Hello’ message since no data has been passed yet:
Accessing the vulnerable application
Now if for example we decided to ping the host ‘127.0.0.1’, we would observe the server’s output execution of the ping response. For this, the parameter ‘?ping=127.0.0.1’ is appended to the URL query string.
Ping to 127.0.0.1
However, if we add a semicolon and another command to the end of the URL, the command is executed by the ‘child_process.exec’ function since it is passed along as a shell command in the newly spawned ‘/bin/sh’ process. For instance, below we concatenated the command ‘cat /etc/passwd’ to read the file from the server.
Command Injection
Untrusted User Input
Untrusted data is most often data that comes from the HTTP request, in the form of URL parameters, form fields, headers, or cookies. However, data that comes from databases, web services, and other sources is frequently untrusted from a security perspective. That is, untrusted data is input that can be manipulated to contain a web attack payload.
Emerging technologies such as Node.js in conjunction with Express.js can suffer from common attack injections such as Cross Site Scripting, SQL Injection or XPath Injection.
Take for example the following snippet of code:
var http = require('http'); var url = require(‘url’); http.createServer(function(request, response) { var parsedUrl = url.parse(request.url, true); response.writeHead(200, {“Content-Type”: ”text/html”}); response.write(“Hello ” + parsedUrl.query.name); response.end(); }).listen(8080);
When accessing the application coded above, first we can observe a ‘Hello’ message as shown in the figure below.
Accessing the vulnerable application
By assigning a value to the variable ‘parsedUrl.query.name’, its content will be reflected on the server’s response.
Accessing the vulnerable application
Given the lack of sanitization in this input, any type of character can be included to be reflected thus allowing embedding HTML or client-side JavaScript code. This is also known as a Cross Site Scripting vulnerability.
Accessing the vulnerable application
Regex DoS
The Regular expression Denial of Service (ReDoS) is a Denial of Service attack, that exploits the fact that most Regular Expression implementations may reach extreme situations that cause them to work very slowly (exponentially related to input size). An attacker can then cause a program using a Regular Expression to enter these extreme situations and then hang for a very long time.
The attack surface for Node.js in regards to loss of availability is quite large, as it deals with a single event loop. If an attacker can control and block that event loop, then nothing else gets done.
For instance, the code below is attempting the common task of validating an email address on the server side.
validateEmailFormat: function( string ) { var emailExpression = /^([a-zA-Z0-9_.-])+@(([a-zA-Z0-9-])+.)+([a-zA-Z0-9]{2,4})+$/; return emailExpression.test( string ); }
Following we can observe the impact of this vulnerability by measuring the response time of every time the regular expression gets executed. With a valid email address the regex took 0,9694442ms.
start = process.hrtime(); console.log(validateEmailFormat("[email protected]")); console.log(process.hrtime(start)); Output: true [ 0, 9694442 ] <- Initial response time
With an invalid email address of 100 characters the regex took 0,49849962ms.
start = process.hrtime(); console.log(validateEmailFormat("jjjjjjjjjjjjjjjjjjjjjjjjjjjj@ccccccccccccccccccccccccccccc.5555555555555555555555555555555555555555{")); console.log(process.hrtime(start)); Output: false [ 0, 49849962 ] <- Initial bad input baseline
With an invalid email address of 101 characters, the regex took 0,55123953ms.
start = process.hrtime(); console.log(validateEmailFormat("jjjjjjjjjjjjjjjjjjjjjjjjjjjj@ccccccccccccccccccccccccccccc.55555555555555555555555555555555555555555{")); console.log(process.hrtime(start)); Output: false [ 0, 55123953 ] <- Added 1 character to the input and you see minimal spike
With an invalid email address of 114 characters, the regex took 8,487126563ms.
start = process.hrtime(); console.log(validateEmailFormat("jjjjjjjjjjjjjjjjjjjjjjjjjjjj@ccccccccccccccccccccccccccccc.555555555555555555555555555555555555555555555555555555{")); console.log(process.hrtime(start)); Output: false [ 8, 487126563 ] <- Added 12 characters and you see it bumps up significantly
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /