I'm still continually baffled that we ended up in a world where automatically accepting updates from every dev and their dog is not just the norm but recommended practice.
I'm still continually baffled that we ended up in a world where automatically accepting updates from every dev and their dog is not just the norm but recommended practice.
I think anyone who thinks they're doing this is fooling themselves. You can review code for accidental vulnerabilities but if someone is trying to slip in a backdoor it shouldn't be hard to do so in a stealthy manner.
The reality is that the entire dependency concept is just broken. There is an implicit trust that all dependencies are equally trusted. Your logging package is just as capable of performing file and network operations as your http package, even if you assume it won't.
That's silly.
It is up to programming languages and package managers to solve these problems. They're also not that hard to solve, in my opinion. "Run arbitrary code on a computer" is a model we've been securing for decades with web browsers, both in terms of web pages and extensions, and now too with mobile.
Solving "this code can do X but that code shouldn't be able to" is similarly easy to solve with languages that support effects or capabilities.
It just hasn't been done yet.
Dependencies with dangerous but necessary permissions can still abuse them: Your network library will still be able to add a bitcoin miner.
What happened if an update requests a new permission?
Also, how would that have prevented the current situation? Infinite loops are famously hard to detect and prevent automatically.
A lot of that stems from permissions systems being implemented outside of the code they constrain. In theory a compiler knows every reachable system call and all points of data input that could reach them, and as such it could constrain the program's capabilities accordingly.
In fact, compilers already do this for control flow integrity - it would just be a more advanced system.
> What happened if an update requests a new permission?
It's going to depend on the system. For browser extensions the new permission means a new prompt, so you'd get a CI failure until a human updated a lockfile.
> Also, how would that have prevented the current situation? Infinite loops are famously hard to detect and prevent automatically.
It really depends on the system. You could have a CPU capability that restricts cycles or forces preemption, etc.
I'm not saying you can solve literally all security problems but you can reduce risk considerably. If "infinite loop" is the scariest thing a dependency can do we're in a pretty good position. An unconditional infinite loop should break your CI tests.
Sorta yes, sorta no.
Imagine I'm making a chat client, and I want users to be able to drag and drop images to share. But the OS doesn't have an "open drag-and-dropped file, extension .png or .jpg" function call, it only has "open file" which lets me open ~/.ssh/id_rsa too.
Or if I'm making a web browser and I want to support U2F tokens. But there's no OS "talk to U2F token" call - the browser needs access to the system calls for "talk to arbitrary USB devices".
Sandboxing PC software is tough.
A programming language doesn't have to expose system calls directly. It arguably it shouldn't, in fact, for exactly this reason.
Further, you can restrict a process to only open specific files in a number of ways on Linux, including based on path. There's room for improvement, though.
I think it follows from two things:
1. We use open source software for everything. This is also true of our dependencies, so we get hundreds or thousands of transitive dependencies. Many of which are presumably written by dogs, because (i) no one can tell if you're a dog on the internet, and (ii) OSS maintainers are so overworked they ask their dogs for help.
2. Languages and libraries are full of footguns, software is full of bugs and therefore vulnerabilities, and no one cares enough to go through the enormous effort to fix things. And this is true through the whole stack. So the only practical way to stay secure-ish is to reactively patch software as vulnerabilities are publicly discovered. And also defence in depth. (I would distinguish between the publicly known time that a vulnerability is discovered, and the first time it was discovered. You hope the two are the same but for many vulnerabilities, if a clever adversary found them first we'd never know.)
With these two things together, you have a ton of questionable dependencies, and you need to update them all the time for security reasons.
> So the only practical way to stay secure-ish is to reactively patch software as vulnerabilities are publicly discovered.
But this is different from just blindly accepting any update that upstream gives you.
> And also defence in depth.
This sounds increasingly like security theater. You can always more layers obstacles to make things harder for malware that is already on your system, but it's not clear to me how much this actually reduces your atrack surface.
1. Reduce blast radius: assuming component X is compromised, how far and wide can it be felt?
2: Principle of least privilege: once compromised, what can X do or access? Extend to the credentials X carries or has access to.
3: Detection: how early and how well can you detect the compromise in previous two steps?
You can never prevent a compromise, but you can make it easier to notice when it has happened, and you can limit what the attackers can do afterwards.
It does help. Quite a bit.
Each layer (e.g. firewall rules that require that all internet access go through a proxy), adds non-trivial amount of work for the hacker to get anything useful done.
1. best case - hacker will give up.
2. good case - you have more time to notice and react.
How much layer cost you, how much does it cost for hacker to overcome it.
Things to remember:
1. Not all hackers are nation states. Most are not.
2. We must accept that no security measure is absolute even against script kidies. Given enough time and luck/misfortune js sandbox will do "rm /sensitive/file".
Recent Log4shell example shows that one can follow all best practices and still get bit in unexpected way.
This leads to some thoughts on statically-compiled applications; while they might have some vulnerable dependency, I suspect that it's harder when the attacker is limited to the app's "baked-in" functionality that defines how those dependencies get used.
Edit: Also, I should note that while I would greatly prefer, and do advise, that they base their environments on minimal OS distributions, this seems rare. The base system patching would be much easier to manage if it started from some BSD-like minimal state, or Alpine Linux, and included only what it needs. Instead, any infrastructure vulnerability assessment leads the teams to chasing down numerous patches in things they have, but never use.
People suck at this. What this actually tends to do is mean "no updates, ever" unless you have a particularly rigorous culture of dependency management.
a culture where upstream writes in more detail what their code is supposed to do and downstream enforces that the software indeed does (not do anything beyond) what they specified
It didn't lead to fewer updates, it led to less usage of SELinux.
Whether or not this would be compatible with the way dependencies are used today is another question.
I think at some point it will have to be a language level feature. The ability to sandbox or provide permissions to packages/functions. Just like our OS had to, just like browsers had to, just like phones had to.
Our code is the platform, the packages the apps. It's a similar use case.
If I could download a module, and tell the compiler this module, and everything it uses (including packages that I also use, but through a different call tree) will never access the network or write to disk, it'd help grant some small peace of mind in terms of security at least.
I was leaning more towards the web approach where we assume everyone is out to get us, but they can't unless we give them that one permission they need. If it's a statically typed language then it'd even allow dependency walking to see what permissions are used at a granular level and we can decide not to bring in anything that's too loose. This of course won't solve cases like logic bugs, but it'd help mitigate the impact.
I'm just not sure if it's even feasible?
Not sure if you can scope down permissions as part of an module import or if it only works when you initialize the interpreter
Deno runs code in a sandbox where you need to give permissions to scripts/modules for them to access local files, the network, etc:
https://deno.land/manual@v1.17.2/getting_started/permissions
> If I could download a module, and tell the compiler this module, and everything it uses (including packages that I also use, but through a different call tree)
Javascript's prototype based inheritance looks like it can help facilitate such conditional submodule invocation. But, and partially for performance reasons, static compiling would be necessary. So Javascript and its dominant NPM package ecosystem can never go in a direction like this.
If only C++ or Python (dynamically typed, I know) had prototypes instead of class based inheritance.
Edit:
Looks like another commenter referenced what we're probably talking about:
> Now, about the technical solution to this. We have this, for well defined programming languages (read: statically typed ones, or dynamically typed ones with a clear structure).
> It's a linker. Tech from the 1950s.
> Link (include) just the stuff you want, "tree shake"/"remote dead code" whatever you don't.
Can we create an open source linker for JavaScript and NPM packages?
Tech cycles with people who reinvent the wheel and keep making the same mistakes
All these problems have long been solved
So invariably, people discovered that if they threw out the complexities of the solutions, they could make faster progress.
Then they eventually ran into the corner cases.
That's the time loop that keeps happening.
As for reinventing the wheel, are wheels really settled science? As far as I know, new kinds of wheels are being created all the time. It's not just that new people are creating them, there are new things wheels need to do every day and new sets of requirements that the old existing wheel designs don't fulfill. Look at wheels from 50 years ago and they are nothing like the wheels of today. The wheels on cars are nothing like the wheels on aircraft, which in turn are nothing like the wheels on trains.
Manually checking before updating does not scale.