Writing Secure Node.js Code - Danny Grander

In his presentation, Danny Grander walked us through hacking a vulnerable Node.js application, as well as looking in-depth into three different vulnerabilities in popular npm packages.

It is a good learning opportunity to see a real-world software, written by experienced developers that had security issues that later got fixed, and hopefully we can learn something from that.

Below you find the presentation video synchronized with the slides, as well as a near-perfect, stylized transcript of the presentation from a first person perspective:

Intro - About NodeConfBP

NodeConfBP is a one-day, single track conference held in Budapest in 2017 January, sponsored and organized by RisingStack - the Node.js Consulting & Development Company.

Meet Danny Grander from Snyk

Okay so, hi everyone and thank you for coming! My name is Danny, I came here from Tel-Aviv and I'm doing security research at Snyk. This is actually my first time here in NodeConf Budapest.

About Danny Grander of Snyk

I'm searching for vulnerabilities in open-source code, and basically building our vulnerability database. Snyk is a devtooling company, we're based in Tel-Aviv and London, and we're building tools for developers to help them consume open-source code securely.

Doggos at the Snyk Office

And we have a lot of office dogs! :)

In my past, I've been doing development in different startups, and I worked as a CTO at a security consultant company, doing security research and crypto-analysis.

I also really enjoy playing capture the flag games with my team.

These are security competitions where different teams try to compete against the other by hacking the systems of the other teams, and defending their own from being hacked. And three weeks ago, my team had the honor to win the CCC capture the flag - an event held in Hamburg, Germany.

npm Usage has Exploded

So, at Snyk we're big fans of Node and npm, and obviously we're not alone. npm usage has exploded, there are more than 380.000 packages on the npm registry, with 6 billion downloads per month and 65 thousand publishers. These are striking numbers, and they keep on growing in a staggering pace.

npm Usage has Exploded

So clearly, Javascript has won. Definitely with the speed of growth and the reach of the community.

A typical Node.js app has thousands of Dependencies

A typical Node application has somewhere between hundreds, up to thousand of dependencies - and as Daniel mentioned eariler - some of them are direct dependencies, (these we see in our package.json files) and some of them are indirect.

The majority of the dependencies we use are actually indirect.

We just pull them in into our application, and making them our own code.

Your code vs. other peoples code in a Node.js app

The orange part here is representing the npm dependencies, the code we pulled in, making it our own.

This is actually a positive slide, because - thanks to npm and the Node community - we can create all this value by writing this little purple circle, and just focusing on the core.

But with that comes a security risk of course.

Do you know if a package writer has security expertise

And the question is:

  • Do we even know what dependencies we pull in, what dependencies we end up with?
  • Do we know if the developers had any security expertise, or if the code went through any security testing?
  • Do we know whether its a well maintained code, and the bugs or the security issues get reported to the maintainers are addressed in a timely manner?

Finally, do we know if every single dependency has any known security vulnerabilities?

14 percent of npm packages carry known vulnerabilities

We tested all npm packages with Snyk, and we found 14% of them carrying known security vulnerabilities, either directly in it's own code, or in one of their dependencies.

And roughly 4 out of 5 Snyk users find vulnerabilities in their own app when testing for the first time.

Hacking a vulnerable Node.js application

So now I'm going to show you a sample vulnerable application and walk through the process of finding these issues and fixing them with Snyk.

Also, we'll be looking in-depth into three different vulnerabilities in popular npm packages.

It will be a good learning opportunity to see a real-world software, written by experienced developers that had a security issue that later got fixed, and hopefully we can learn something from that.

The slide contains the URLs and the application, this demo application, which is available on our github.

Demo Setup for Goof. the vulnerable Node.js application

So this is Goof, a demo MVC todo app. It's pretty simple.

We can add to-do items, we can emphasize things, we can use markdown here, so it's really basic.

We have some awesome about page here, and if you look at the package.json file of this app, it has 20 dependencies. Not too many, pretty standard.

Goof the todo app written in Node

So the first step I'd like to do is to go to Snyk and test my GitHub repositories for the known vulnerabilities in the npm packages.

Goof vulnerability page in Snyk

So once again, it's a demo application, which has sixteen vulnerabilities. And if you go to see the details or the report, you can see that there are the list of the vulnerabilities sorted by severity

We can see the name of the package, the vulnerable version, how it was introduced to our application, and some description about the vulnerability.

And now, I'm going to show you how to hack it in three different ways!

#1: Directory Traversal Hack - the insecure st npm package

The first example I will use is the st module.

The st module

st is an express middleware used to serve static files. Basically, these are the files that the JavaScript, the CSS and the images that our application serves.

Adding the st module to the nodejs app

We required it here just in this line, and we provided two options. One is the path from which folder it can serve the files, and the other is the URL.

st vulnerability explained in snyk

You can see that there is a path traversal vulnerability in st. So let's try to exploit it. Let's switch to the terminal.

$ curl https://goof-nodeconf-budapest.herokuapp.com/public/about.html
<!DOCTYPE html>
<html>
    <h1>The BESTest todo app evar</h1>
</html>

So first thing I'll try to do is to fetch the about page, works as expected.

But as an attacker I'll try to escape the folder right?

So I'll do the ../../ and hopefully in the end reach the root folder and go for the /etc/passwd for example.

$ curl https://goof-nodeconf-budapest.herokuapp.com/public/../../../../../../etc/passwd
Cannot GET /etc/passwd

If I run that it actually fails, and the reason is that st protects against this kind of attempts.

It filters out, normalizes the path and prevents the escaping of the folder, but it misses something, and that's where we can encode the dots, with URL encoding.

$ curl https://goof-nodeconf-budapest.herokuapp.com/public/%2e%2e/
<!doctype html><html><head><title>Index of </title></head><body><h1>Index of </h1><hr><a href="../">../</a>
<a href="exploits/">exploits/</a>           2017-01-21T00:41:42.000Z          -
<a href="node_modules/">node_modules/</a>       2017-01-21T00:41:53.000Z          -
<a href="public/">public/</a>             2017-01-21T00:41:42.000Z          -
<a href="routes/">routes/</a>             2017-01-21T00:41:42.000Z          -
<a href="views/">views/</a>              2017-01-21T00:41:42.000Z          -
<a href="app.js">app.js</a>              2017-01-21T00:41:42.000Z       1903
<a href="app.json">app.json</a>            2017-01-21T00:41:42.000Z        267
<a href="db.js">db.js</a>               2017-01-21T00:41:42.000Z        893
<a href="package.json">package.json</a>        2017-01-21T00:41:42.000Z       1070
<a href="README.md">README.md</a>           2017-01-21T00:41:42.000Z       1334
<a href="utils.js">utils.js</a>            2017-01-21T00:41:42.000Z        641
<hr></body></html>%

We just have to type %2e %2e, and repeat this multiple times. We see that we're getting to the root folder of the app.

$ curl https://goof-nodeconf-budapest.herokuapp.com/public/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin/sh
news:x:9:9:news:/var/spool/news:/bin/sh
uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh
proxy:x:13:13:proxy:/bin:/bin/sh
www-data:x:33:33:www-data:/var/www:/bin/sh
backup:x:34:34:backup:/var/backups:/bin/sh
list:x:38:38:Mailing List Manager:/var/list:/bin/sh
irc:x:39:39:ircd:/var/run/ircd:/bin/sh
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
u37116:x:37116:37116:,,,:/app:/bin/bash
dyno:x:37116:37116:,,,:/app:/bin/bash

We see the package JSON file and the source path. We can actually look at them here, and I can go for the db.js file and the source code.

But of course I can go for the root folder with /etc/passwd.

And again, it's not the most sensitive problem in the system.

The password hashes are stored in the shadow file, but still you see that the attacker can access any file that the Node process has access to.

So this was the first one, an easy one, so let's look at another vulnerability..

#2: Content & Code Injection (XSS) Hack - the insecure marked npm package

The second one is a package call marked. Who is familiar with marked?

marked npm package

This is an npm package used for rendering a markdown, and this is how our todo app is able to do all this fancy stuff.

the XSS vulnerability in marked

So markdown actually supports HTML, and it tries to prevent from injecting script code.

What you see in marked is a Cross-Site Scripting vulnerability, so the first thing we could try to do is something like that:

<script>alert(1)</script>

The markdown actually protects against this, and it has a security component called sanitizer.

failed xss attempt

It's not turned on by default, but in this app we actually turned it on and it protects us against this kind of things.

[Gotcha](javascript:alert(1))

But what we can try to do is create a link in markdown formats, and it looks something like this.

xss with markdown gotcha

And again, it didn't work because it was anticipated and prevented here by the developers.

But luckily, we can create an actually vulnerable version of this link:

[Gotcha](javascript&#58this;alert(1&#41;)

So what we do here is using HTML entities encoding.

We added the the semicolon, and it also requires the closing brackets also to be encoded - so you can see that it gets a little tricky. It's not always obvious how to exploit these things, but when I do that, and click on the link we actually get down where we want.

sucessful xss hack with markdown

alert message

So yeah, this was a markdown.

#3: Remote Memory Exposure Hack - the insecure mongoose npm package

And the last one I want to show you is the mongoose library. Who here is familiar with Mongoose? Oh, yeah almost everybody..

So our app uses a MongoDB to store the TODO items, and it has a pretty basic schema.

mongoose buffer in goof

If you look at the DB file here, we see that the content, the extra content of the todo item is stored in a buffer, and if you click through the vulnerability details here, we see that it is a Remote Memory Exposure vulnerability and it has to do with buffer, with how buffer behaves in Node.

remote memory exposure vulnerability in mongoose

So I switch to the terminal and let's take a look at the Buffer.

Buffer can be constructed from strings, or arrays, and when we do so from a string, we see that basically binary Buffers are created with the ASCII values of the value provided in a constructor.

So we can set something like this..

Node Buffers - part 1

..and it works as expected. It also accepts arrays, and again creates a buffer by the buffer of these values.

Node Buffers - part 2

It also accepts integers in the constructor, and who knows what will happen when I run this?

> new Buffer (100)

Okay, so a new buffer will be created with the length of 100, but we see that it holds something inside, we see some data. And if you are doing it again, you see some different data.

Node Buffers - part 3

So what happens is that for historical and performance reasons, we get a buffer of length 100 but this is uninitialized.

Basically what we see here is already used heap memory, previously used heap memory and if you do it again, we just get to see more of the heap memory.

And the reason it happens is that typically when we ask for a buffer the next thing we do is to populate that buffer with some data, so the developers in this case saved us the time, saving the CPU cycles for initializing this Buffer.

And this is really common in lower-level languages like C and C++ but very unexpected in JavaScript.

And that's how this bevahior led to about 5 or 6 vulnerabilities in different npm packages.

It's a documented behaviour, it's not a vulnerability in Node..

So going back to our todo app, if I just somehow can pass an integer to the application instead of a string as my TODO item I would hopefully be able to see the heap memory of the process.

100 passed as string instead of integer

So here I wrote the number 100, obviously it would pass as a string to the application, but as an attacker what I'll try to do is to make the application accept this number as an integer.

passing a string as an integer

So let's try to do that.

I'll again switch to the terminal and I'm going to use an utility called HTTP, its called HTTPie but the command is HTTP. I'm going to submit that form from the command line to the application.

So what I'm doing is do something like content=Buy beer to HTTP -- form as it is a form submission, just going to copy the URL and the endpoint here is create, and I’m about to ask for the -v verbose version.

buy beer form submission

If we just refresh the app, we can see that a new item was added and everything works as expected.

I can also submit this data as JSON, as content type JSON, because the application uses the JSON body parser, so I can go here and use the JSON syntax to submit this form.

I'll change it to JBeer here, and I'll just add —json so the content type of the post request would be application JSON. So let's try that, and refresh.

json beer added

So I did that and it was added, and if we switch back to the terminal we can see that this time we submitted the request as application JSON with this data.

So as soon as I use JSON I can actually now control the type, and if I change this to 800 you can already see that much more data came back.

Leaking Node.js Memory

todo app leaking

But if I refresh we see the uninitialized memory object parsed:

uninitialized memory object parsed

And actually the fun part here is that we see some string of the source code and in this case it probably was const.

Let's repeat this for like one hundred times, and pipe it in some file.

So I do that, and in a second terminal I'm going to see the hex dump a bit. So this is the real time memory coming back from the node process of the Heroku servers, and if I stop here, again I can actually see some source code.

repeat for success

So the fix for this vulnerability is actually really simple. You just have to verify that when the type is a number, it's just turned it into an array of a singe item with that item, and that's it.

the mongoose fix

Fixing Vulnerable npm packages, Writing Secure Node.js Code

What we've seen is the first step, we basically looked at vulnerabilities, we tested our application and the next step would be to fix them.

opening a fix pr in snyk

At Snyk we do that with fix PR. We can just click here, we see all the list of the vulnerabilities.

So, we can see all the details of the vulnerabilities that we upgraded the packages from, and also the patches we applied to.

But we cannot get rid of some vulnerabilities with upgrades. Why?

Because basically we pull in dependency A, dependency A pulls B, B pulls C, C has a vulnerability so we are using the latest version of A, we cannot change anything, so we kind of need the developers of B or C to upgrade to the latest version of the vulnerable package.

So in this case what we do is that we have this prepublish hook that runs this command and it basically looks at the vulnerable packages and applies a patch to them. So we backport those patches and apply as soon as npm install finished to run.

And the last thing we can do is to watch the projects. Basically when new vulnerabilities are disclosed, even when we did not change any code in our application, we want to be notified about it.

Snyk takes a snapshot of the dependencies of that project and when a new vulnerability is reported we just send an alert, an email, a slack notification or again, a fix pull request.

And also we can have the test hook here so every time a new pull request is created, we are testing the changes, the dependencies for new vulnerabilities.

Right, so switching back to the slides..

JavaScript Takeaways

So, there are some JavaScript takeaways:

We've seen three examples of vulnerabilities but obviously there are many more and if you go to snyk.io/vuln, (our vulnerability database) you can see that this is an opportunity to learn from many other vulnerabilities in open-source packages.

more resources from snyk

And that's it, I'll be around to answer the questions! (In the comments section below as well!)

enjoy secure nodejs

Question: what's your relationship with the Node Security Project? Are you using their database, or are you contributing to that, and what's the difference between using nsp check and using Snyk?

Answer: So first, we're good friends with nsp and Adam Baldwin.

Our database contains all the vulnerabilities their database has, along with the vulnerabilities we add by our own research. For example we added above a dozen new vulnerabilities in the last month.

We also automatically scan all github npm projects for things that look like reports or fixes of vulnerabilities, so we look at commit messages, we look at issues which are open..

The difference between the two products I would say is the fix part. So basically, both of us kind of detect the issue, but we want to also fix that.

So this is not just like kind of "this is the list of the vulnerabilities, we created more work for you".

This is also our attempt to do the fix, the upgrades and the patches, so we've seen that briefly, but this is our focus, we want to help you fix the issue!