> There is nothing special about computer that allows us to put complete trust into any piece of code, and that is okay. Instead, we strive to build trust among developers so that we don't have to read every line of code in every level of abstraction to make sure our files won't be deleted.
I don't understand why this is so often treated as a dichotomy, where either you trust every single author to not delete your files, or you review every line of code to make sure it doesn't delete your files. Both seem unsustainable. I think we need to look at the problem from a different perspective: why is every single transitive dependency able to delete your files?
What functionality does node-ipc have that requires it to have unfettered access to the filesystem? Well, with its current implementation, it needs to be able to create a unix socket for e.g. 'serve()'. But if you changed the API a bit, you could conceivably pass it a file handle to a unix socket that you've just created, instead. Now that module doesn't need any arbitrary filesystem access at all! You could make similar changes to those methods responsible for making outbound network connections, etc. At that point, you could almost completely sandbox this module, because all it needs is what you've given it.
In other words: let's apply the principle of least authority for real.
There are a lot of people who think subdeps should be able to just assert that they’re not vulnerable at all. It’s a popular idea, enough so it might be implemented in Node. Even though the originator of the idea was receptive to my many objections, the RFC authors were not.
Here’s a thought that’s been living in my head rent free ever since: what if we don’t have transitive dependencies at all? What if we just… package them all up together? That sounds absurd, but we are almost all using a build system, we who are all have sourcemaps and dead code elimination baked in. We can standardize this. The package NPM delivers is already different from the published thing. We can define “this is how you reproduce my build”, and distributors can say “this is what is present in this package” with high confidence. Sourcemaps can deduplicate.
I'm not very familiar with how Node dependencies work, but isn't this just reshuffling the same card deck?
If package M is malicious, what difference does it make if it's required directly by your app, or through a sub-dependency chain, like A -> B -> C -> M?
If you have an approach that allows you to vet all your direct dependencies, why wouldn't that allow you to vet sub-dependencies as well?
What I’m proposing is specifically in cases where sub-dependencies may have a known vulnerability but that isn’t in any of the call paths of your direct dependency. It’s an alternative to the “audit assertions”[1] proposal, which I find problematic for reasons I discussed there before I bowed out. My idea is that you can be confident you’re not affected by a vulnerability in a dependency (at any depth), if that vulnerability is no longer in the code in the first place.
It also reduces the surface area to vet in the first place. It’s highly likely many dependencies will be stripped down considerably, if not outright deduplicated or eliminated. The “npm installs thousands of dependencies” thing is a real problem, but it’s also partly because it’s installing stuff you’ll never actually execute in any way.
You can pare down sub-dependencies with confidence, because you already know what code paths are hit by the parent dependency at packaging time. You can’t do that with direct dependencies until you go to package/deploy, because of course you may expand your usage of their APIs during development.
This sounds like a good idea to me. My shallow understating of the Node ecosystem makes me think of tree-shaking. Is that something else? I was under the impression that it allowed not shipping pieces of the dependencies which are unused, which should help with limiting the surface area to vet.
From my superficial understanding of how this works, couldn't the "build" phase take note which parts of dependencies are loaded, and then compare those with where the known vulnerabilities are? I realize some issues may be bigger or harder to narrow down, but it still looks like it would be a step in the right direction.
I think it could go further than the state of the art tree shaking, by adding deduplication of unchanged code between different versions of the same package. This would mostly be a benefit for shrinking installed dependencies, but that helps make the idea of actually vetting/auditing them a lot more plausible too. And it could help make dealing with semver-incompatible versions more flexible (i.e. if the remaining code is identical, tada now it's semver-compatible because it’s the same code!), somewhat mitigating a common audit resolution blocker.
All of this of course would need some sort of a manifest detailing which code comes from which location(s) in which package(s), and (at least optionally) a human-friendly file system structure that users can actually audit/vet.
If I didn’t already have a backlog of side projects a mile long, I’d probably be working on this already, because there’s a clear need for improvement in this area. But none of it feels outlandishly hard to pull off if anyone wanted to run with the idea.
If you find this resonates, Lavamoat is basically the continuation of that 2018 blog post. Hopefully it catches on to get enough traction that SES (or something similar enough providing an API for controlling capabilities) makes it into runtimes.
Sure, but languages aren't designed to provide secure isolation on a module-by-module level. The encapsulation that exists (e.g. private fields) isn't for security.
Or is there some security feature of node or JS that I'm missing?
You’re right that in most languages, encapsulation isn’t strong enough to be used for security in this way. However, there are languages in which it is; scheme I believe has strict enough lexical scope and encapsulation to be used like this. (And of course there are languages designed specifically for this purpose, though they’re not often used: E and Monte come to mind.)
Of the popular languages, though, JavaScript is the closest to being strict enough for this style of programming, and there’s been active work for a while towards that goal:
Sandboxed components are good at mitigating attacks from the outside. However they don’t help much against a malicious package author who has a multitude of different ways of causing havoc: while(1); for example. Playing this game of cat and mouse will add so much red tape that nobody will want to use that package manager anymore.
Ultimately package repos run on trust and a low level of risk is tolerated because of the benefits of being able to get stuff done.
> Playing this game of cat and mouse will add so much red tape that nobody will want to use that package manager anymore.
Of course. But your comment still reads as "we can't make it perfect, so might as well not try at all". I think there can still be value in limiting the blast radius.
Your while(1), if present, is likely to be intentionally malicious. But I've seen many naïve libraries implement "accidentally quadratic" algorithms, which is basically a disguised while(1), but unintentional.
The point is that the end result of `rm -rf /` is painful. It doesn't matter if someone managed to maliciously sneak that into your supply chain, or if it rises from some funky corner case of some package whose only goal is to bring world peace. So being able to prevent some leftpad lib from accessing the file system at all makes a lot of sense.
AFAIK, Deno seeks to solve the most severe case of this problem in Node[0]. At the very least, you start from a place where permission surprises are more readily caught. Would be really great to be able to specify on a per-import basis.
Is there an entity behind the code / package / library that you are using that you can hold Liable in case of ill-behaviour.
Don‘t use gcc for mission critical stuff.
Use a compiler that is backed by people who are responsible for the compilers behaviour by contract. Greenhills / Hightec Alliums Tasking Compiler. The list goes on and on.
As for chips: cooperate with the manufacturer. Specific your requirements for the chips behaviour. Get it manufactured. If it is available off the shelf and matches your spec, go on and buy off the shelf but TEST if it actually behaves like stated on the sales PowerPoint slide. If not report back to the manufacturer and get it fixed.
Until now, there was a social process of trust and reputation that was good enough.
At the moment, supply chain attacks seem to be mainstream enough that a normally trustworthy person could consider doing them, so trust becomes harder.
This is why you don't accept or encourage antifragile/vigilante/cowboy/etc behaviour, or any kind of sabotage of infrastructure.... now it's cool and trendy. There's millions of wannabe unabombers who just need any excuse to disrupt tech.
Remember that guy who wanted people to suddenly pull npm packages without warning?
Tech culture used to be way more awesome. Now it's a bunch of people who wish they were doing pure mathematics or random weekend tinkering, who hate their industry, and would be perfectly happy to see a lot of it gone, and a forced return to the days of paper notes and brass keys with no tracker.
26 comments
[ 0.22 ms ] story [ 63.8 ms ] threadAt least I assume that's what the headers are referencing haha
I don't understand why this is so often treated as a dichotomy, where either you trust every single author to not delete your files, or you review every line of code to make sure it doesn't delete your files. Both seem unsustainable. I think we need to look at the problem from a different perspective: why is every single transitive dependency able to delete your files?
Take a look at the node-ipc method reference: https://www.npmjs.com/package/node-ipc#ipc-methods
What functionality does node-ipc have that requires it to have unfettered access to the filesystem? Well, with its current implementation, it needs to be able to create a unix socket for e.g. 'serve()'. But if you changed the API a bit, you could conceivably pass it a file handle to a unix socket that you've just created, instead. Now that module doesn't need any arbitrary filesystem access at all! You could make similar changes to those methods responsible for making outbound network connections, etc. At that point, you could almost completely sandbox this module, because all it needs is what you've given it.
In other words: let's apply the principle of least authority for real.
https://medium.com/agoric/pola-would-have-prevented-the-even...
Here’s a thought that’s been living in my head rent free ever since: what if we don’t have transitive dependencies at all? What if we just… package them all up together? That sounds absurd, but we are almost all using a build system, we who are all have sourcemaps and dead code elimination baked in. We can standardize this. The package NPM delivers is already different from the published thing. We can define “this is how you reproduce my build”, and distributors can say “this is what is present in this package” with high confidence. Sourcemaps can deduplicate.
If package M is malicious, what difference does it make if it's required directly by your app, or through a sub-dependency chain, like A -> B -> C -> M?
If you have an approach that allows you to vet all your direct dependencies, why wouldn't that allow you to vet sub-dependencies as well?
It also reduces the surface area to vet in the first place. It’s highly likely many dependencies will be stripped down considerably, if not outright deduplicated or eliminated. The “npm installs thousands of dependencies” thing is a real problem, but it’s also partly because it’s installing stuff you’ll never actually execute in any way.
You can pare down sub-dependencies with confidence, because you already know what code paths are hit by the parent dependency at packaging time. You can’t do that with direct dependencies until you go to package/deploy, because of course you may expand your usage of their APIs during development.
1: https://github.com/npm/rfcs/pull/422
From my superficial understanding of how this works, couldn't the "build" phase take note which parts of dependencies are loaded, and then compare those with where the known vulnerabilities are? I realize some issues may be bigger or harder to narrow down, but it still looks like it would be a step in the right direction.
I think it could go further than the state of the art tree shaking, by adding deduplication of unchanged code between different versions of the same package. This would mostly be a benefit for shrinking installed dependencies, but that helps make the idea of actually vetting/auditing them a lot more plausible too. And it could help make dealing with semver-incompatible versions more flexible (i.e. if the remaining code is identical, tada now it's semver-compatible because it’s the same code!), somewhat mitigating a common audit resolution blocker.
All of this of course would need some sort of a manifest detailing which code comes from which location(s) in which package(s), and (at least optionally) a human-friendly file system structure that users can actually audit/vet.
If I didn’t already have a backlog of side projects a mile long, I’d probably be working on this already, because there’s a clear need for improvement in this area. But none of it feels outlandishly hard to pull off if anyone wanted to run with the idea.
https://github.com/LavaMoat/LavaMoat
https://news.ycombinator.com/item?id=30707096
Or is there some security feature of node or JS that I'm missing?
Of the popular languages, though, JavaScript is the closest to being strict enough for this style of programming, and there’s been active work for a while towards that goal:
https://agoric.com/blog/technology/ses-securing-javascript/
Ultimately package repos run on trust and a low level of risk is tolerated because of the benefits of being able to get stuff done.
while(1) - kill -9 / Task Manager / Activity Monitor
All files deleted - cry.
Of course. But your comment still reads as "we can't make it perfect, so might as well not try at all". I think there can still be value in limiting the blast radius.
Your while(1), if present, is likely to be intentionally malicious. But I've seen many naïve libraries implement "accidentally quadratic" algorithms, which is basically a disguised while(1), but unintentional.
The point is that the end result of `rm -rf /` is painful. It doesn't matter if someone managed to maliciously sneak that into your supply chain, or if it rises from some funky corner case of some package whose only goal is to bring world peace. So being able to prevent some leftpad lib from accessing the file system at all makes a lot of sense.
In SES, that is controlled by allowing assignment of a budget and failing if it's exceeded. Similar to how gas works in the EVM.
https://github.com/crev-dev/
[0] https://deno.land/manual/getting_started/permissions
critical stuff has to be defined per application.
For controlling aircraft engines or car engines I for sure as hell do not accept unreliable packages.
That’s why contracts exist that hold software suppliers LIABLE for faulty software.
At the moment, supply chain attacks seem to be mainstream enough that a normally trustworthy person could consider doing them, so trust becomes harder.
This is why you don't accept or encourage antifragile/vigilante/cowboy/etc behaviour, or any kind of sabotage of infrastructure.... now it's cool and trendy. There's millions of wannabe unabombers who just need any excuse to disrupt tech.
Remember that guy who wanted people to suddenly pull npm packages without warning?
Tech culture used to be way more awesome. Now it's a bunch of people who wish they were doing pure mathematics or random weekend tinkering, who hate their industry, and would be perfectly happy to see a lot of it gone, and a forced return to the days of paper notes and brass keys with no tracker.