Closed feross closed 8 years ago
@ChALkeR
Once again: that will only make things worse, even from the security point of view.
No, It won't. The only "reasons" I've seen mentioned for that sound a lot more like excuses than legitimate concerns, and certainly not worth holding up what is a pretty important security fix for.
I'm not satisfied with adding a --zero-fill-buffers
flag as the only backported fix for this issue. I'm a big believer in secure defaults, and this is not one.
I'll say it again: Do not implement a fail-deadly. Stop giving your users footguns.
@mscdex
Benchmarks comparing Buffer(n)
vs Buffer(n).fill(0)
are not realistic, because they compare not initializing the buffer at all, ever (which you shouldn't do - that the vulnerability) with initializing the buffer once, while the valid real-world case is initializing the buffer once (in your code by overwriting unitialized memory) vs initializing the buffer twice (by overwriting zeroed memory). Therefore, in real world worst-case slowdown can't be worse than 2x.
In OS X malloc
is only ~15% slower than calloc
if you overwrite the buffer with some data. For large allocations OS can use zero page copy-on-write memory, which makes zeroing almost free (and when you read pages that were left unchanged it's even slightly faster due to improved cache locality).
The difference in performance is tiny - anything done with the buffer in JS is likely to completely eclipse cost of zeroing that can be done very efficiently in low-level code.
I'm strongly in favour of security over IMHO inconsequential performance cost, and would prefer Buffer
to be secure and deterministic.
For me uninitialized Buffer
was also a source of bugs: I forgot to fully overwrite a buffer, but my program seemed to work, because I was lucky to get uninitialized values that didn't trigger an error. Of course it broke later. If Buffer
was deterministic, it'd either always fail or always work, which I prefer.
Even new systems programming languages (D, Rust, Swift) don't allocate uninitalized memory by default any more.
I'd prefer zeroing to be the default behavior, treated as a security issue, and backported to old versions. If someone doesn't update - that's the same problem as if they don't apply any other security patch.
I'm going to sidestep comment on the proposals as I can see it's getting heated and I think almost all the views are being represented - but I would like to add another voice representing a long term node developer where this thread is the first I knew of this behaviour and being surprised. That may also be unimportant, there are many areas we don't touch as developers - but as the lead on a node codebase in light of this I'd want to go back and make sure we got this right.
@pornel Zero-filling memory or not has nothing to do with how deterministic the API works. The fact that uninitialized memory is returned is also well documented. So the most you can say is that it was a surprise because you weren't fully aware of how Buffer behaved.
Myself and several other CTC members have agreed that we are open to discussion of how to improve the "surprise" of the Buffer API, along with how to best address this in a forward looking and backwards compatible way. But this isn't going to happen in a couple days, and probably not a couple weeks. This is a substantial API change and will be treated as such. It's likewise a major version change, and taking the release schedule into account there's ample time to sit down and work out what's best going forward.
@trevnorris @feross @mafintosh
Note that I split the proposal into two parts β the old one that was previosly discussed and the Buffer.from(value) one. Those could be discussed separately and merged with different schedules.
@trevnorris Thanks for the update - think that's all many of us could have asked for.
@trevnorris
we are open to discussion of how to improve the "surprise" of the Buffer API
Thanks for making that point clear. That's all I intended by opening this issue. :+1:
As far as the "inconsequential performance cost"... here are some current benchmarks comparing buffer creation using #4682's zalloc()
vs. alloc()
(hint: larger numbers are better, zalloc == zero-filled allocated, vs alloc == the current uninitialized behavior):
method | size | op/s |
---|---|---|
zalloc | 10 | 1218.96236 |
alloc | 10 | 3354.52777 |
method | size | op/s |
---|---|---|
zalloc | 1024 | 1076.14519 |
alloc | 1024 | 2101.91937 |
method | size | op/s |
---|---|---|
zalloc | 2048 | 648.60563 |
alloc | 2048 | 991.04239 |
method | size | op/s |
---|---|---|
zalloc | 4096 | 441.87567 |
alloc | 4096 | 516.88185 |
method | size | op/s |
---|---|---|
zalloc | 8192 | 298.96463 |
alloc | 8192 | 412.11662 |
These clear demonstrate that the performance difference is not inconsequential by any stretch of the imagination. As @trevnorris and I and the other CTC members here have indicated, we are certainly open to making improvements here (as demonstrated by the work in #4682) but it will take a while and there is quite a bit to consider before making any firm decisions.
Is performance really more important than security?
Let me put it another way: If a developer introduces a vulnerability in their application because of how Buffer
is implemented, who do you blame?
Seriously. What are your priorities? I made a similar argument against PHP last year:
There was serious resistance to throwing an Exception in the event of "our system is totally fucked and no CSPRNG is available". They'd rather just return
false
because it would be more consistent.Then there were the "No it should be a critical
E_ERROR
that terminates the script." Which would make developers nervous about adopting the new interfaces ("this can make my app crash randomly with no recourse? No, I'll stick with an insecure random number generator").I argued that an Exception would be a good balance of secure-by-default but allowing users to handle failures gracefully.
The relevance here: As developers, we're often tasked with juggling multiple seemingly-opposed requirements at once. There were many seemingly valid arguments (consistency! no, we need to always fail!).
The path PHP chose, which I argue is the path Node.js should consider, is to be secure by default but allow developers who really want to trade away security for some other metric (performance, etc.) to opt out of this default.
try {
$str .= chr(random_int(0, 255));
} catch (Exception $e) {
// Look ma, I'm living dangerously!
$str .= chr(mt_rand(0, 255));
}
Make the constructor sane by default, but let developers who need an unsafe but more performant API endpoint opt into that by changing the way their code is written.
Is it a BC break? Yes! But secure code is necessarily incompatible with insecure code, lest it open the door to downgrade attacks.
More concise recommendation: Migrate the way Buffer
is currently implemented to UnsafeBuffer
and then fix Buffer
.
@paragonie-scott That will make things worse even from the security point of view, and moreover will not solve an important part of this issue. Please wait for my post regarding the proposal, it explains my statements.
That will make things worse even from the security point of view,
Explain how migrating the insecure API to an explicitly labelled UnsafeBuffer
and then fixing Buffer
to behave more securely will make things worse from a security point of view.
@ChALkeR
That will make things worse even from the security point of view
Can you please clarify exactly how it'll make things worse?
Make the constructor sane by default, but let developers who need an unsafe but more performant API endpoint opt into that by changing the way their code is written.
This seems like a healthy best-of-both-worlds approach
This is getting a bit too heated. It would help tremendously if we did a few things:
@mikeal, do you have actual code with actual measurements where Buffer(number) is the rate-limiting step? Of course, there are several other approaches code that does this can take to regain the performance, such as using the opt-in methods that will be provided. By contrast code which is written with Buffer(object) will result in a security failure whenever object becomes a number. There is no correctness issue with clearing new buffers, just a minor speed issue
@wbl you have to look at the performance cost in aggregate. For instance: not too long ago I was talking with someone who needs to run 5 docker instances (one per network device + 1 extra) for a networking project he's running on that is based on Raspberry Pi. He wasn't going to be able to use Node.js because of the memory constraints but when our idle memory usage dropped significantly in v4 he was able to use Node.js. Cutting in half the number of operations we can do per second here is likely to throw a new wrench in that project and this is just one example I happen to know about because I started chatting with him at a conference. Node.js is now the most widely supported runtime in IoT platforms and is running on-chip in nearly all next generation platforms. These environments are highly constrained and something we have to fight hard to keep in mind as we have historically been server focused.
@jasnell: Adding
Buffer.safe
andBuffer.unsafe
to LTS versions does nothing to help unmaintained code either given that none of that code will be modified to use the new APIs. Back porting those APIs does not solve the problem.
My proposal is unrelated to 'unmaintained code'. The unmaintained code will be broken anyway, that is a given - there's no point optimizing for that usecase beyond the --safe-buffers
flag that was already discussed, as the code itself will never ever get updated.
The usecase I am proposing this for is for maintained, third-party code that needs to run on older LTS/maintenance releases. When faced with this memory issue, a module developer (not application developer!) has several possible ways to approach this in their module:
Buffer.safe
and/or Buffer.unsafe
. This will break their module in anything but the newest versions, affecting their userbase. They will almost certainly not pick this option for that reason alone.Buffer(num)
for as long as they can get away with it - ie. until it gets hard-deprecated. This is an attractive option - they can file it away as "look at this later", and from a functional point of view, their code will still keep working, on both old and new versions. It is also very dangerous.In practice, the only reasonable option for a module developer is to keep using the unsafe behaviour. Only the most security-conscious people who actually understand the issue (it is not simple to understand!) will spend more than a few seconds assessing the impact of using a polyfill and how they can make it a viable option. The rest of the ecosystem remains vulnerable.
If you backport the safe
and unsafe
methods, however, the tradeoffs change. You can now safely use the new methods in your module code, and expect users to have them in their environment - it is much more viable to tell your users to upgrade to the latest minor release on their branch, than to tell them to upgrade to a new, major, breaking version. This is a tradeoff between version compatibility and security that many module developers will consider reasonable.
In short: my proposal is not about patching a security issue directly in older versions. It's about providing the right tools in the right versions, to drive 'secure behaviour' from module developers, and make it worry-free for them to migrate to the new API. This is a necessity for this to truly get fixed. You need that ecosystem adoption.
@rvagg: To all those calling this a bug: this is not a bug, it is intended behaviour and is documented, it is not a surprise to anyone who works in core and is common amongst non-web programming environments.
Yes, it is absolutely a bug. The Buffer constructor is overloaded, where some methods of invocation cause safe behaviour, and others cause unsafe behaviour. That is a design flaw, and thus a bug. The actual memory-related vulnerabilities in eg. ws
are just a consequence of that bug.
@rvagg: Just please stop interpreting this discussion as some newly discovered heartbleed-like situation, that's unhelpful hyperbole that will be dismissed when folks are making considered decisions here.
It is a new Heartbleed-like situation. This is not an edge case. It affects literally almost everything that takes typed user input (eg. anything with a JSON API). If anything, it's worse - because now you can freely define exactly how much memory you want to get back.
@seishun: Personally, I use new Buffer(number) a lot in my code. I wouldn't mind replacing those calls with Buffer.alloc(number), but I would mind having the word "unsafe" all over my code that doesn't even send data anywhere. Not every Node.js program is a web server, and the concept of "safety" depends heavily on the use case.
It doesn't. In this context, "safe" and "unsafe" refers to memory safety. A non-zeroed alloc
call is memory-unsafe. Whether that has implications for your application is a separate discussion entirely.
The 'unsafe' naming is appropriate and necessary. If nothing else, it is a warning flag to people working on your code, indicating that they need to be careful what they're doing. Which is the case in any case where Buffer
is used, because your Buffer-filling code may well stop working when the code is changed.
That having been said, I don't particularly care whether it's called safe
/unsafe
or alloc
/allocUnsafe
. As long as the unsafe option is clearly marked as such, and does not look like the obvious option. Similarly, new Buffer.fill(0)
is not an acceptable option, as it is not the obvious option.
@mikael: This would mean that individual modules decide whether they want to be faster or safer and would leave users of modules with a combination of slower and less safe modules when they want to choose a specific characteristic.
No such trade-off exists here. Your module being "safe" is not optional. The only thing this change does, is make you explicitly decide whether you want to let the Buffer
module handle the safety, or whether you want to roll your own (possibly more performant) implementation for it. In either case, using uninitialized memory in an application-unsafe manner is never acceptable, regardless of the environment.
To give an example: If you are using a Buffer
to store a bitmap, and you know and ensure that you won't access segments before they have been written to, then that's fine. Your code is still safe - as long as it enforces that - but it is more performant, as you don't have the now-unnecessary zerofill.
So, again, this explicitly isn't about a trade-off of security vs. performance. It's about making a user to decide whether to take care of the security aspect themselves, and defaulting to doing it for them if the user has no idea. This has no effect on embedded/low-powered platforms.
@jasnell: The new APIs are now named Buffer.alloc() and Buffer.zalloc().
Those names do not sufficiently convey the safety characteristics. I guarantee you that any new developer to Node is going to be confused about this, and pick alloc
because it's shorter and looks "less special".
@trevnorris: The fact that uninitialized memory is returned is also well documented.
Whether this is 'documented' is irrelevant. No amount of documentation will sufficiently change user expectations. When desiging a potentially dangerous API, you must design it so that even if the user doesn't read the documentation, the worst thing that'll happen is that performance or availability is affected, but never security.
For a practical example of this, look at nearly every bit of cryptography code written in the past two decades. Almost without exception, it's broken because it uses cryptographic primitives incorrectly, despite being documented just fine. On the other hand you have libsodium/NaCL, which is designed to be secure-by-default (without removing access to lower-level functionality), and I can count on none fingers the amount of incorrect implementations I have seen for that.
@mikael: Stop imagining things like "if we built it today." In order to move towards a resolution we have to move towards a shared understanding of the reality of the project and the constraints it has in terms of stability with such a massive ecosystem and userbase.
While I understand your sentiment, I partially disagree. Yes, what the ecosystem looks like right now is important - but that is step two. Step one is to determine what it should look like if we started from a blank canvas, and after that, we need to start considering how close we can get to that without breaking the ecosystem. The second step does not and cannot replace the first - it will just result in poorly-implemented patches and bikeshedding.
From @jasnell 's measurements it's a 15% to 60% cost depending on size. Is avoiding that cost really worth the subtle trap? @feross has demonstrated that it exists in real-world code, it's exploitable on the open web right now.
@mikeal, Getting pwnd on IoT devices also matters. Furthermore, we aren't cutting performance in half. Even if we are reducing the performance of new Buffer(number) by 50%, the overall application's performance will be affected much less, as it doesn't spend 100% of its time in new Buffer(number). Again, do you actually have numbers to back up your assertions that new Buffer(number) is extremely critical?
@trevnorris: we are open to discussion of how to improve the "surprise" of the Buffer API, along with how to best address this in a forward looking and backwards compatible way
well this is good to hear! my 2 cents: can we start documenting node::Buffer
C++ API?
I think that would help and the C++ ought be documented in general (different issue)
@feross @mafintosh @alfiepates @paragonie-scott @karissa @wbl
Everyone, https://github.com/ChALkeR/notes/blob/master/Lets-fix-Buffer-API.md β this is my current proposal. That article covers many opinions and questions on the issue, please make sure that you read it before asking further questions and proposing anything else (it might be already covered).
@jasnell, @trevnorris, @mikeal, ptal.
@feross Do I get it right that your proposal on the start of this page does not represent your current point of view anymore? If so, could you update it? Another variant: move to a separate thread and discuss the current proposal from the beginning.
I am going to get some sleep now, it's 5:50 here. Will be back in ~10 hours. Please read my note.
I think this discussion has gone off the rails a bit.
To limit scope, I think we should stop considering zero-filling or changing the Buffer
constructor in any way for now. Instead, let's add two new functions:
Buffer.from(value)
- convert from many types to a bufferBuffer.alloc(size)
- create an uninitialized buffer with given sizeGoing forward, we can all use Buffer.from(val)
the way we currently use Buffer(val)
, and Buffer.alloc(size)
when we need uninitialized memory.
This would have prevented the issues that affected ws
and bittorrent-dht
because Buffer.from(number)
throws instead of revealing uninitialized memory.
That's it.
@feross This would still require eventual deprecation of the Buffer constructor. There's a bit of an obsession with constructor functions in the Node.js community, in my experience - especially amongst new developers (coming from other languages), who are the most affected by this issue.
If new Buffer
is not deprecated, then people will just keep using the constructor (also in part because of the compatibility reasons I explained earlier).
Not fixing the constructor means all existing code will be exposed to issue, plus new code written based on old code. Why isn't backporting to 5.x a valid answer? It seems like an easy fix is being avoided for spurious reasons.
@joepie91 I think any solution that involves changing Buffer
will have to happen slowly. We can't change this overnight because of the impact on the ecosystem.
The best outcome, in my opinion: We add Buffer.from
and Buffer.alloc
to node v4, v5, and future versions, and update the docs to recommend using these new constructors.
A long, long time from now, when most of the ecosystem has migrated, we can consider zeroing out the memory from Buffer(number)
, or removing Buffer, or whatever seems right at that time.
At least with Buffer.from
and Buffer.alloc
we have a clean way forward, where developer intent is expressed clearly and new security bugs of this type won't be written.
I think any solution that involves changing Buffer will have to happen slowly. We can't change this overnight because of the impact on the ecosystem.
I understand that. I am definitely not advocating breaking changes to older versions, for example.
The best outcome, in my opinion: We add Buffer.from and Buffer.alloc to node v4, v5, and future versions, and update the docs to recommend using these new constructors.
I'd say it should probably do more than 'recommend' - deprecation does not necessarily mean that the functionality has to be removed straight away, but it can definitely help to encourage people to move away from it. The deprecation notice in the PHP manual played an important role in getting people to transition from mysql_*
to PDO, for example, even though it took years for it to actually get removed.
Aditionally, the (non-breaking) backporting would still have to occur for all maintained versions, including 0.10 and 0.12 branches, for the reasons I explained before. In reality, many module authors still support 0.10 and 0.12, and will not use the new methods unless they work across the board.
A long, long time from now, when most of the ecosystem has migrated, we can consider zeroing out the memory from Buffer(number), or removing Buffer, or whatever seems right at that time.
I would now argue against automatic zeroing, for the reason @ChALkeR described. This would remain a possible issue even in the long run.
At least with Buffer.from and Buffer.alloc we have a clean way forward, where developer intent is expressed clearly and new security bugs of this type won't be written.
There would have to be a reason for people to migrate to the new methods, though. Especially for those who come from classical languages and are biased towards the use of constructors (as somebody who helps out people in #Node.js on a daily basis - this is a very serious and very common problem), the old, insecure approach would still look more attractive. Even if there are safer alternatives.
Either a lot of emphasis needs to be put onto the safety consequences of the constructor, or it must be made very clear that using the constructor is not a viable longer-term approach because of eg. deprecation.
Further, @thejefflarson raises a concern regarding what V8 expects in the message for this commit (documentation reference is provided at the bottom of that page). I'm not qualified to comment on the validity of this, but it seems plausible.
@feross has a good point. Because of the type differentials programmers don't
have a "best practice" to avoid using Buffer(number)
when they want
Buffer(anything but number)
. Separating this from the whole zero-fill
issue this would be good to put into people's hands.
On Thu, Jan 14, 2016 at 7:50 PM, Sven Slootweg notifications@github.com wrote:
Further, @thejefflarson https://github.com/thejefflarson raises a concern regarding what V8 expects in the message for this commit https://github.com/thejefflarson/node/commit/f563aa8a58ca01c2425bd6c7226593360c707934 (documentation reference is provided at the bottom of that page).
β Reply to this email directly or view it on GitHub https://github.com/nodejs/node/issues/4660#issuecomment-171865589.
This is a security issue and as such, I believe a fix absolutely should be devised for all LTS releases. FWIW, the original proposal should actually be safe to backport to LTS. Any situation that is relying on that memory to not be zeroed is a security issue in itself and should not be supported anyway.
I think we actually should be zeroing the memory in new Buffer(number) and use a flag to mark it as zeroed until a write happens, so double zeroing would not have a performance impact, then change core to use the unsafe Buffer.allocate(number) where appropriate.
Frankly, expecting users to upgrade to newer major releases just to get a security fix is unacceptable. Many will not upgrade due to inability to quickly make the appropriate code changes, lack of resources to audit the major release at short notice, or even lack of awareness a security issue exists due to only tracking status of the particular LTS they are on.
Consider this: a security patch should, by definition, be a breaking change. It is the removal of a behaviour that has dangerous consequences. A removal is always a breaking change because someone could be relying on that wrongness. Rather than letting them continue in their blissful ignorance of how terrible the thing are doing is, they should be forced to fix it.
I would however suggest getting @chrisdickinson to run his magical npm module finding thing to search for any modules using the single argument buffer initializer and have them evaluated to actually see if it breaks anything in userland, rather than making baseless assumptions about it.
As for the performance argument: yes, performance is great, speed is a good thing to strive for. You know what's even better than a fast server? A server that hasn't been hacked.
@Qard "a security patch should, by definition, be a breaking change" is so incredibly wrong it beggars belief. The vast majority of security fixes break unwanted/unused behaviours (e.g. Buffer overflows). Speaking with my Red Hat on we go out of our way not to break ABI/API compatibility (because doing so will for sure result in a DoS of all the software that suddenly breaks).
Now in this specific case we have a generally well understood security issue: giving random chunks of memory data to someone without zeroing them out when they request a buffer is known to be a problem/bad idea because most apps don't sanitize memory when they are done (they simply release it).
A perfect and timely example of this is from today's CVE-2016-0777 (the OpenSSH client bug):
If the OpenSSH client connects to an SSH server that offers the key exchange algorithm "resume@appgate.com", it sends the global request "roaming@appgate.com" to the server, after successful authentication. If this request is accepted, the client allocates a roaming buffer out_buf, by calling malloc() (and not calloc()) with an out_buf_size that is arbitrarily chosen by the server:
Had calloc been used...
@kurtseifried Zeroing the memory in the same API everyone already uses does not break API compatibility. It only removes the ability to do a thing you should not have been allowed to do in the first place. It's a "breaking" change in that behaviour differs, but it differs in a way that it should.
But if a program frees up memory containing private data without zeroing it out first then THAT program is the one with a security bug, not the next one allocating and reading the memory...
The discussions and benchmarks around zero-filling by default have all explored an implementation which does .fill(0)
. This has been shown to be not insignificant slower.
There has also been some mentions of using new Buffer(new TypedArray)
, or the low level calloc
syscall. Is anyone able to explore the perf impacts of these approaches? If it's possible to get zero-filled within an order of magnitude, do most of the objections go away?
On the docs issue: if I have a string and want to create a buffer, I can read the docs: https://nodejs.org/api/buffer.html#buffer_new_buffer_str_encoding Each constructor is documented separately, and I only think I need to handle strings. It's entirely possible for me to have read the docs for the functionality I'm actually using, but miss the fact there is an overload for number. For this reason I support the idea of introducing from/allow or similar, deprecating and removing the existing constructor from the docs.
This issue reminds me of the Ruby YAML issue from a while back: http://tenderlovemaking.com/2013/02/06/yaml-f7u12.html
Similarities:
.load
method that allowed RCE with untrusted input (I think this was documented), it later gained a .safe_load
.@Blub True, but that doesn't mean that we can just fob the responsibility off to whomever to avoid doing the things we are capable of doing to improve security.
Security is everyone's responsibility.
Zeroing on destruction is super unpredictable in a GC'd environment. For native buffers, the memory is not necessarily completely unreachable at the point of collection, so it needs to be the responsibility of the destructing code to clean itself up. This is just an unfortunate trait of C/C++ that leaks into the implementation of the JS side of buffers. A buffer can't safely free the memory it points to. It's an unsafe construct by design, really. But we have the opportunity to make it a bit safer here.
Let me contribute some little thoughts on this:
new Buffer(number)
and new Buffer(string)
have the same signature. new Buffer(string)
Β is very safe to use, but new Buffer(number)
might cause a memory disclosure. Buffer
. The problem relies on this missed check, because calling new Buffer(string).fill(0)
does not make any sense. This is the problem ws
had, and the most critical one.malloc
in C. We can argue that a decision was made in the past, and that decision was wrong. Unfortunately we have to live with malloc()
.Completely alternative option: remove new Buffer(string)
in favor of new Buffer(string, enc)
, which is now safe (thanks to https://github.com/nodejs/node/pull/4514). Bonus points: this requires no new APIs.
All of this to note that the solution to this problem might not be zero-filling all buffers, and other solutions might be possible as well.
@glenjamin ... the PR #4682 does not use the fill(0)
any more to implement the zero-filled buffer and it includes benchmarks. The approach take by #4682 is to essentially fallback to using Uint8Array when a zero-filled buffer is requested. This uses the calloc
syscall under the covers. This approach also has the disadvantage of not being pool-allocated. The combination of the no-pooling + calloc makes the zero-filled buffer anywhere from about 20-60% slower than the non-zero-filled alternative, depending on the size of buffer being allocated.
@jasnell Thanks for the clarification, is it feasible to try going a level deeper, and putting some malloc/calloc logic in node_buffer.cc
directly?
@mcollina Your solution will not solve anything right now, because any removal would require a very long time to actually happen. Giving the users a new API will allow them to fix their code without waiting for the removal of new Buffer(value)
. Thus, the solution could not consist only of the removal, and has to introduce new, safer API.
@mcollina
Completely alternative option: remove new Buffer(string) in favor of new Buffer(string, enc), which is now safe (thanks to #4514). Bonus points: this requires no new APIs.
That's what I suggested way back in https://github.com/nodejs/node/issues/4660#issuecomment-171722581. This would cover all the both vulnerabilities presented so far.
@ChALkeR
Giving the users a new API will allow them to fix their code without waiting for the removal of new Buffer(value).
They can already use new Buffer(string, enc)
, and in the next major version it's guaranteed to throw if they accidentally pass a number.
@seishun too many comments, sorry I've missed that. :+1: for this solution, even though it makes the code a little bit more wordy, from new Buffer('hello')
to new Buffer('hello', 'utf8')
.
@ChALkeR the only thing that would be needed in v4 (I would not touch anything else) is a deprecation warning.
@alfiepates perhaps, but zeroing isn't free and you're basically cleaning up after a mess left by a tiny fraction of programs. Most of the time the allocated buffer will be worthless. I can understand your point of view, though.
So, I went ahead and benchmarked my change in #4706 using Benchmark.js. There is a slight performance hit, but it may be negliglible: most likely around 3.09% but as little as 0.15%.
https://gist.github.com/thejefflarson/bb6a7c651cddd305a18f
At this point I don't see the harm in switching to calloc. The current situation makes both Buffer
and ArrayBuffer
instances unsafe.
(edited to fix the math)
So, I went ahead and benchmarked my change in #4706 using Benchmark.js
Node.js has a series of benchmarks for Buffer, you should run those in order to quantify the difference. You'll find them in the benchmarks directory under buffers :)
@mikeal thanks, updated the gist with those numbers. Sometimes it's faster, sometimes slower.
The current situation makes both
Buffer
andArrayBuffer
instances unsafe.
If by ArrayBuffer
, you mean TypedArray
(s), this isn't true. We reset the flag so TypedArrays are zero-filled.
@Fishrock123 oh, yes I see.
Another vuln, now fixed and public: Mongoose vulnerability β assigning a number to the property that is Buffer
-typed saves unitialized memory block to the DB. POC.
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.