Securing against filesystem hacking exploits
For a Node app to be insecure, there must be something an attacker can interact with for exploitation purposes. Due to Node's minimalist approach, the onus is on the programmer to ensure that their implementation doesn't expose security flaws. This recipe will help identify some security risk anti-patterns that could occur when working with the filesystem.
Getting ready
We'll be working with the same content
directory as we did in the previous recipes. But we'll start a new insecure_server.js
file (there's a clue in the name!) from scratch to demonstrate mistaken techniques.
How to do it...
Our previous static file recipes tend to use path.basename
to acquire a route, but this ignores intermediate paths. If we accessed localhost:8080/foo/bar/styles.css
, our code would take styles.css
as the basename
property and deliver content/styles.css
to us. How about we make a subdirectory in our content
folder? Call it subcontent
and move our script.js
and styles.css
files into it. We'd have to alter our script and link tags in index.html
as follows:
<link rel=stylesheet type=text/css href=subcontent/styles.css> <script src=subcontent/script.js type=text/javascript></script>
We can use the url
module to grab the entire pathname
property. So let's include the url
module in our new insecure_server.js
file, create our HTTP server, and use pathname
to get the whole requested path as follows:
var http = require('http'); var url = require('url'); var fs = require('fs'); http.createServer(function (request, response) { var lookup = url.parse(decodeURI(request.url)).pathname; lookup = (lookup === "/") ? '/index.html' : lookup; var f = 'content' + lookup; console.log(f); fs.readFile(f, function (err, data) { response.end(data); }); }).listen(8080);
If we navigate to localhost:8080
, everything works great! We've gone multilevel, hooray! For demonstration purposes, a few things have been stripped out from the previous recipes (such as fs.exists
); but even with them, this code presents the same security hazards if we type the following:
curl localhost:8080/../insecure_server.js
Now we have our server's code. An attacker could also access /etc/passwd
with a few attempts at guessing its relative path as follows:
curl localhost:8080/../../../../../../../etc/passwd
Note
If we're using Windows, we can download and install curl
from http://curl.haxx.se/download.html.
In order to test these attacks, we have to use curl
or another equivalent because modern browsers will filter these sort of requests. As a solution, what if we added a unique suffix to each file we wanted to serve and made it mandatory for the suffix to exist before the server coughs it up? That way, an attacker could request /etc/passwd
or our insecure_server.js
file because they wouldn't have the unique suffix. To try this, let's copy the content
folder and call it content-pseudosafe
, and rename our files to index.html-serve
, script.js-serve
, and styles.css-serve
. Let's create a new server file and name it pseudosafe_server.js
. Now all we have to do is make the -serve
suffix mandatory as follows:
//requires section ...snip...
http.createServer(function (request, response) {
var lookup = url.parse(decodeURI(request.url)).pathname;
lookup = (lookup === "/") ? '/index.html-serve' : lookup + '-serve';
var f = 'content-pseudosafe' + lookup;
//...snip... rest of the server code...
For feedback purposes, we'll also include some 404
handling with the help of fs.exists
as follows:
//requires, create server etc fs.exists(f, function (exists) { if (!exists) { response.writeHead(404); response.end('Page Not Found!'); return; } //read file etc
So, let's start our pseudosafe_server.js
file and try out the same exploit by executing the following command:
curl -i localhost:8080/../insecure_server.js
We've used the -i
argument so that curl
will output the headers. The result? A 404
, because the file it's actually looking for is ../insecure_server.js-serve
, which doesn't exist. So what's wrong with this method? Well it's inconvenient and prone to error. But more importantly, an attacker can still work around it! Try this by typing the following:
curl localhost:8080/../insecure_server.js%00/index.html
And voilà! There's our server code again. The solution to our problem is path.normalize
, which cleans up our pathname
before it gets to fs.readFile
as shown in the following code:
http.createServer(function (request, response) {
var lookup = url.parse(decodeURI(request.url)).pathname;
lookup = path.normalize(lookup);
lookup = (lookup === "/") ? '/index.html' : lookup;
var f = 'content' + lookup
}
Note
Prior recipes haven't used path.normalize
and yet they're still relatively safe. The path.basename
method gives us the last part of the path, thus removing any preceding double dot paths (../
) that would take an attacker higher up the directory hierarchy than should be allowed.
How it works...
Here we have two filesystem exploitation techniques: the relative directory traversal and poison null byte attacks. These attacks can take different forms, such as in a POST request or from an external file. They can have different effects—if we were writing to files instead of reading them, an attacker could potentially start making changes to our server. The key to security in all cases is to validate and clean any data that comes from the user. In insecure_server.js
, we pass whatever the user requests to our fs.readFile
method. This is foolish because it allows an attacker to take advantage of the relative path functionality in our operating system by using ../
, thus gaining access to areas that should be off limits. By adding the -serve
suffix, we didn't solve the problem, we put a plaster on it, which can be circumvented by the poison null byte.
The key to this attack is the %00
value, which is a URL hex code for the null byte. In this case, the null byte blinds Node to the ../insecure_server.js
portion, but when the same null byte is sent through to our fs.readFile
method, it has to interface with the kernel. But the kernel gets blinded to the index.html
part. So our code sees index.html
but the read operation sees ../insecure_server.js
. This is known as null byte poisoning. To protect ourselves, we could use a regex
statement to remove the ../
parts of the path. We could also check for the null byte and spit out a 400 Bad Request
statement. But we don't have to, because path.normalize
filters out the null byte and relative parts for us.
There's more...
Let's further delve into how we can protect our servers when it comes to serving static files.
Whitelisting
If security was an extreme priority, we could adopt a strict whitelisting approach. In this approach, we would create a manual route for each file we are willing to deliver. Anything not on our whitelist would return a 404
error. We can place a whitelist array above http.createServer
as follows:
var whitelist = [ '/index.html', '/subcontent/styles.css', '/subcontent/script.js' ];
And inside our http.createServer
callback, we'll put an if
statement to check if the requested path is in the whitelist array, as follows:
if (whitelist.indexOf(lookup) === -1) { response.writeHead(404); response.end('Page Not Found!'); return; }
And that's it! We can test this by placing a file non-whitelisted.html
in our conten
t directory and then executing the following command:
curl -i localhost:8080/non-whitelisted.html
This will return a 404
error because non-whitelisted.html
isn't on the whitelist.
Node static
The module's wiki page (https://github.com/joyent/node/wiki/modules#wiki-web-frameworks-static) has a list of static file server modules available for different purposes. It's a good idea to ensure that a project is mature and active before relying upon it to serve your content. The node-static
module is a well-developed module with built-in caching. It's also compliant with the RFC2616 HTTP standards specification, which defines how files should be delivered over HTTP. The node-static
module implements all the essentials discussed in this chapter and more.
For the next example, we'll need the node-static
module. You could install it by executing the following command:
npm install node-static
The following piece of code is slightly adapted from the node-static
module's GitHub page at https://github.com/cloudhead/node-static:
var static = require('node-static'); var fileServer = new static.Server('./content'); require('http').createServer(function (request, response) { request.addListener('end', function () { fileServer.serve(request, response); }); }).listen(8080);
The preceding code will interface with the node-static
module to handle server-side and client-side caching, use streams to deliver content, and filter out relative requests and null bytes, among other things.
See also
The Preventing cross-site request forgery recipe discussed in Chapter 8, Implementing Security, Encryption, and Authentication
The Setting up an HTTPS web server recipe in Chapter 8, Implementing Security, Encryption, and Authentication
The Hashing passwords recipe discussed in Chapter 8, Implementing Security, Encryption, and Authentication
The Deploying an app to a server environment recipe discussed in Chapter 11, Taking It Live