Node.js Buffer knows everything.

December 12, 2015 · View on GitHub

Tl;dr: if your server-side code, under any circumstances, leaks uninitialized Buffers to the client (including partially uninitialized), you are doomed.

Those Buffers will contain http traffic to/from other clients, your db passwords, private certificates, your app source code and the content of configuration files.

If you fully understand that and are aware of how to prevent it — you could stop reading this note now, there will be nothing new for you here. I have seen several people who are not aware of that, so this note will be used as a reference in such situations.

So, what's the reason for that?

Buffer objects, unlike TypedArrays, are not zero filled if created with a new Buffer(size) constructor (or its alias Buffer(size)). The memory that they use as the underlying storage could (and will) contain stuff that there was before the creation of this Buffer, most importantly — parts of other, previosly used Buffer objects that were garbage collected.

Also it should be taken into an account that pretty much every standard I/O operation in Node.js uses Buffers — reading a file, requiring a module, receiving network traffic, passing stuff to the crypto module.

Testcases

Hardcoded variable:

var token = 'paSsWord!ASD, totally secret!';
for (var step = 0; step < 100000; step++) {
    var buf = (new Buffer(200)).toString('ascii');
    if (buf.indexOf(token) !== -1) {
        console.log('Found at step ' + step + ': ' + buf);
    }
}

Comment (notice how this prints parts of your source):

var token = 'pass' + 'word!ASD';
//password!ASD
for (var step = 0; step < 100000; step++) {
    var buf = (new Buffer(100)).toString('ascii');
    if (buf.indexOf(token) !== -1) {
        console.log('Found at step ' + step + ': ' + buf);
    }
}

Crypto:

var crypto = require('crypto');
var token = 'password' + Math.random().toString(32);
crypto.pbkdf2(token, 'salt', 1, 2, function(err, result) { console.log(result.length) } )
for (var step = 0; step < 10000; step++) {
    var buf = (new Buffer(200)).toString('ascii');
    var ind = buf.indexOf(token);
    if (ind !== -1) {
        console.log('Found at step ' + step + ': ' + buf);
    }
}

This is also true for split files, loading configuration files, http traffic, etc.

How to avoid leaks?

Make sure that all your Buffer objects are initialized.

The best way would be to allocate Buffers in a form of var buf = new Buffer(size).fill(0) — in this case you can immediately see that the buffer is initialized on the exact same line. This is useful for performing quick checks using grep to find where Buffers are manually allocated.

If you are optimizing for speed in a hot code path, and are 100% sure what you are doing — it's perfectly fine that you use just new Buffer(size) and then fill in each byte manually. But please double check that you don't abort and return an partially unitialized Buffer in case of something going wrong (any error, etc).

An example of bad code:

function makeBufferFromData(data) {
  var buf = new Buffer(data.length * 2);
  data.
}

You should definitely avoid sending uninitialized Buffer objects over the network, even just for testing purposes.

Can this be checked automatically using a static analyzer?

Perhaps, but I am not aware of any static analyzers that do this. Maybe implementing an eslint plugin would be a good idea, but I am not going to do that. Ping me, if you are aware of such a plugin or are making one.

Node.js v4.1.0 issue.

Node.js v4.1.0 had an issue with TypedArrays being not zero-filled under some circumstances. This was first introduced in v4.1.0 and then immediately fixed in v4.1.1, so v4.1.0 was the only affected version.

Refs: