Closed feross closed 8 years ago
Another issue, now fixed and public, in node-floody
by @soldair: https://github.com/soldair/node-floody/commit/6c44722312131f4ac8a1af40f0f861c85efe01b0. POC: var f = require('floody')(process.stdout); f.write(1000); f.stop();
.
Sequelize also had this issue, now fixed and public: https://github.com/sequelize/sequelize/commit/cbfaa4f0a135cfc55874c9bfc39ead2d85c417e9
https://github.com/nodejs/node/pull/4682 has landed adding safe constructors for v6.
Wow, epic :rocket: – amazing job!
Excellent!
Related: #5799.
Hey folks!
If you followed this issue, then you're probably like me and want to start using Buffer.from
and Buffer.alloc
in the new code you write. But if you're also trying to support Node.js v4.x and earlier versions, then you need to check if these functions exist before using them, since old Node.js versions lack support.
There's a better way! You can use safe-buffer
.
safe-buffer
implements the new Buffer APIs for Node.js versions back to v0.10. In newer Node.js versions, the default Buffer
implementation is used.
Hope someone finds this useful!
tl;dr
This issue proposes:
new Buffer(number)
to return safe, zeroed-out memoryBuffer.alloc(number)
Update: Jan 15, 2016
Upon further consideration, I think that returning zeroed out memory is a separate issue. The core issue is: unsafe buffer allocation should be in a different API.
I now support adding two APIs:
Buffer.from(value)
- convert from any type to a bufferBuffer.alloc(size)
- create an uninitialized buffer with given sizeThis solves the core problem that affected
ws
andbittorrent-dht
which isBuffer(variable)
getting tricked into taking a number argument.Why is
Buffer
unsafe?Today, the node.js
Buffer
constructor is overloaded to handle many different argument types likeString
,Array
,Object
,TypedArrayView
(Uint8Array
, etc.),ArrayBuffer
, and alsoNumber
.The API is optimized for convenience: you can throw any type at it, and it will try to do what you want.
Because the Buffer constructor is so powerful, you often see code like this:
_But what happens if
toHex
is called with aNumber
argument?_Remote Memory Disclosure
If an attacker can make your program call the
Buffer
constructor with aNumber
argument, then they can make it allocate uninitialized memory from the node.js process. This could potentially disclose TLS private keys, user data, or database passwords.When the
Buffer
constructor is passed aNumber
argument, it returns an UNINITIALIZED block of memory of the specifiedsize
. When you create aBuffer
like this, you MUST overwrite the contents before returning it to the user.Would this ever be a problem in real code?
Yes. It's surprisingly common to forget to check the type of your variables in a dynamically-typed language like JavaScript.
Usually the consequences of assuming the wrong type is that your program crashes with an uncaught exception. But the failure mode for forgetting to check the type of arguments to the
Buffer
constructor is more catastrophic.Here's an example of a vulnerable service that takes a JSON payload and converts it to hex:
In this example, an http client just has to send:
and it will get back 1,000 bytes of uninitialized memory from the server.
This is a very serious bug. It's similar in severity to the the Heartbleed bug that allowed disclosure of OpenSSL process memory by remote attackers.
Which real-world packages were vulnerable?
bittorrent-dht
@mafintosh and I found this issue in one of our own packages,
bittorrent-dht
. The bug would allow anyone on the internet to send a series of messages to a user ofbittorrent-dht
and get them to reveal 20 bytes at a time of uninitialized memory from the node.js process.Here's the commit that fixed it. We released a new fixed version, created a Node Security Project disclosure, and deprecated all vulnerable versions on npm so users will get a warning to upgrade to a newer version.
ws
That got us wondering if there were other vulnerable packages. Sure enough, within a short period of time, we found the same issue in
ws
, the most popular WebSocket implementation in node.js.If certain APIs were called with
Number
parameters instead ofString
orBuffer
as expected, then uninitialized server memory would be disclosed to the remote peer.These were the vulnerable methods:
Here's a vulnerable socket server with some echo functionality:
socket.send(number)
called on the server, will disclose server memory.Here's the release where the issue was fixed, with a more detailed explanation. Props to @3rd-Eden for the quick fix. Here's the Node Security Project disclosure.
What's the solution?
It's important that node.js offers a fast way to get memory otherwise performance-critical applications would needlessly get a lot slower.
But we need a better way to signal our intent as programmers. When we want uninitialized memory, we should request it explicitly.
Sensitive functionality should not be packed into a developer-friendly API that loosely accepts many different types. This type of API encourages the lazy practice of passing variables in without checking the type very carefully.
Buffer.alloc(number)
The functionality of creating buffers with uninitialized memory should be part of another API. We propose
Buffer.alloc(number)
. This way, it's not part of an API that frequently gets user input of all sorts of different types passed into it.How do we fix node.js core?
We sent a PR (merged as
semver-major
) which defends against one case:In this situation, it's implied that the programmer intended the first argument to be a string, since they passed an encoding as a second argument. Today, node.js will allocate uninitialized memory in the case of
new Buffer(number, encoding)
, which is probably not what the programmer intended.But this is only a partial solution, since if the programmer does
new Buffer(variable)
(without anencoding
parameter) there's no way to know what they intended. Ifvariable
is sometimes a number, then uninitialized memory will sometimes be returned.What's the real long-term fix?
We could deprecate and remove
new Buffer(number)
and useBuffer.alloc(number)
when we need uninitialized memory. But that would break 1000s of packages. So that's a no-go.Instead, we believe the best solution is to:
new Buffer(number)
to return safe, zeroed-out memoryBuffer.alloc(number)
This way, existing code continues working and the impact on the npm ecosystem will be minimal. Over time, npm maintainers can migrate performance-critical code to use
Buffer.alloc(number)
instead ofnew Buffer(number)
.Conclusion
We think there's a serious design issue with the
Buffer
API as it exists today. It promotes insecure software by putting high-risk functionality into a convenient API with friendly "developer ergonomics".This wasn't merely a theoretical exercise because we found the issue in some of the most popular npm packages.
Eventually, we hope that node.js core can switch to this new, safer behavior. We believe the impact on the ecosystem would be minimal since it's not a breaking change. Well-maintained, popular packages would be updated to use
Buffer.alloc
quickly, while older, insecure packages would magically become safe from this attack vector.