Whoa, I didn't know about this:
# Run with restricted file system access
node --experimental-permission \
--allow-fs-read=./data --allow-fs-write=./logs app.js
# Network restrictions
node --experimental-permission \
--allow-net=api.example.com app.js
Looks like they were inspired by Deno. That's an excellent feature. https://docs.deno.com/runtime/fundamentals/security/#permiss...
I very much dislike such features in a runtime or app.
The "proper" place to solve this, is in the OS. Where it has been solved, including all the inevitable corner cases, already.
Why reinvent this wheel, adding complexity, bug-surface, maintenance burden and whatnot to your project? What problem dies it solve that hasn't been solved by other people?
For years, I heard it's better to use cron, because the problem was already solved the right way(tm). My experience with cron has been about a dozen difficult fixes in production of cron not running / not with the right permission / errors lost without being logged / ... Changing / upgrading OSes became a problem. I since switched to a small node script with a basic scheduler in it, I had ZERO issues in 7 years. My devs happily add entries in the scheduler without bothering me. We even added consistency checks, asserts, scheduled one time execution tasks, ... and now multi server scheduling.
Deployments that need to configure OSes in a particular way are difficult (the existence of docker, kubernetes, snap are symptoms of this difficulty). It requires a high level of privilege to do so. Upgrades and rollbacks are challenging, if ever done. OSes sometimes don't provide solution when we go beyond one hardware.
If "npm start" can restrain the permissions to what it should be for the given version of the code, I will use it and I'll be happy.
If cron is broken for you, than the logic solution would be to replace it with something that does work for you. But do so at the right place and abstraction. That's hardly ever the runtime or in the app.
Do One Thing (and do it well).
A special domain specific scheduler microservice? One of the many Cron replacements? One of the many "SaaS cron"? Systemd?
This problem has been solved. Corner cases ironed out. Free to use.
Same for ENV var as configurations (as opposed to inventing yet another config solution), file permissions, monitoring, networking, sandboxing, chrooting etc. the amount of broken, insecure or just inefficient DIY versions of stuff handled in an OS I've had to work around is mind boggling. Causing a trice a loss: the time taken to build it. That time not spent on the business domain, and the time to them maintain and debug it for the next fifteen years.
I rather have my configuration centralized. Instead of configuring two things, this allows me to configure one. I’d take that trade off here.
This is a nice idea, but what do you do when the OS tooling is not that good? macOS is a good example, they have OS level sandboxing [0], but the docs are practically nonexistent and the only way to figure it out is to read a bunch of blog posts by people who struggled with it before you. Baking it into Node means that at least theoretically you get the same thing out of the box on every OS.
[0] https://www.karltarvas.com/macos-app-sandboxing-via-sandbox-...
Thank you for this link, it saves me a lot of searching.
Except, the OS hasn’t actually solved it. Any program you can run can access arbitrary files of yours and it’s quite difficult to actually control that access even if you want to limit the blast radius of your own software. Seriously - what software works you use? Go write eBPF to act as a mini adhoc hypervisor to enforce difficult to write policies via seLinux? That only even works if you’re the admin of the machine which isn’t necessarily the same person writing the software they want to code defensively.
Also modern software security is really taking a look at strengthening software against supply chain vulnerabilities. That looks less like traditional OS and more like a capabilities model where you start with a set of limited permissions and even within the same address space it’s difficult to obtain a new permission unless your explicitly given a handle to it (arguably that’s how all permissions should work top to bottom).
This is what process' mount namespace is for. Various container implementations use it. With modern Linux you don't even need a third-party container manager, systemd-nspawn comes with the system and should be able to do that.
The problem with the "solutions" s.a. the one in Node.js is that Node.js doesn't get to decide how eg. domain names are resolved. So, it's easy to fool it to allow or to deny access to something the author didn't intend for it.
Historically, we (the computer users) decided that operating system is responsible for domain name resolution. It's possible that today it does that poorly, but, in principle we want the world to be such that OS takes care of DNS, not individual programs. From administrator perspective, it spares the administrator the need to learn the capabilities, the limitations and the syntax of every program that wants to do something like that.
It's actually very similar thing with logs. From administrator perspective, logs should always go to stderr. Programs that try to circumvent this rule and put them in separate files / send them into sockets etc. are a real sore spot of any administrator who'd spent some times doing his/her job.
Same thing with namespacing. Just let Linux do its job. No need for this duplication in individual programs / runtimes.
The part you're overlooking is how easy a vulnerability within the application can escape & do damage. Such vulnerabilities could either be someone hacking the application or a supply chain vulnerability. Namespacing & similar techniques limit the blast radius of a compromised process on the rest of the OS, but do nothing to limit the blast radius of a compromise on the assets accessible by the process. For example, if I have a document editor and want to open documents on my OS, namespacing doesn't help - the document editor traditionally needs the ability to open and list files.
Comprehensive capability protection is needed so that you actually need to have a token to do something privileged even within the process. What that looks like is the OS shows a file dialog and gives the process a descriptor (with a random ID) to that file. Similarly, network I/O would need a privileged descriptor the OS gives the application. Then even if you compromise the process you have to fully compromise the process to find the token to do privileged actions with.
"Any program you can run". But you should not run any program as YOU. Programs can run as separate limited user.
You need FreeBSD's Capsicum in your life. It's like what you describe.
How would you do this in a native fashion? I mean I believe you (chroot jail I think it was?), but not everyone runs on *nix systems, and perhaps more importantly, not all Node developers know or want to know much about the underlying operating system. Which is to their detriment, of course, but a lot of people are "stuck" in their ecosystem. This is arguably even worse in the Java ecosystem, but it's considered a selling point (write once run anywhere on the JVM, etc).
> How would you do this in a native fashion?
I dunno how GP would do it, but I run a service (web app written in Go) under a specific user and lock-down what that user can read and write on the FS.
For networking, though, that's a different issue.
> but not everyone runs on *nix systems
Meaning Windows? It also has file system permissons on an OS level that are well-tested and reliable.
> not all Node developers know or want to know much about the underlying operating system
Thing is, they are likely to not feel up for understanding this feature either, nor write their code to play well with it.
And if they at some point do want to take system permissions seriously, they'll find it infinitely easier to work with the OS.
So a separate user for every application I run?
Just locally, that seems like a huge pain in the ass... At least you can suggest containers which has an easier interface around it generally speaking.
I didn't know Windows has that feature, someone please explain
OpenBSD pledge
https://man.openbsd.org/pledge.2
> The "proper" place to solve this, is in the OS.
This is my thought on using dotenv libraries. The app shouldn’t have to load environment variables, only read them. Using a dotenv function/plugin like in omz is far more preferable.
Entirely.
The argument often heard though is 'but windows'. Though if windows lacks env (or Cron, or chroot, etc) the solution would be to either move to an env that does support it, or introduce some tooling only for the windows users.
Not build a complex, hierarchical directory scanner that finds and merges all sorts of .env .env.local and whatnots.
On dev I often do use .ENV files, but use zenv or a loadenv tool or script outside of the projects codebase to then load these files into the env.
I use .env files and have a small bash script to load them into env for dev, or load them into a k8s configmap for deployment.
In a team setting, it can be extremely helpful to have env/config loading logic built into the repo itself. It does not mean it has be loaded by the application process, but it can be part of the surrounding tooling that is part of your codebase.
Yes, that's indeed the right place, IMO: ephemeral tooling that leverages, or simplifies OS features.
Tooling such as xenv, a tiny bash script, a makefile etc. that devs can then replace with their own if they wish (A windows user may need something different from my zsh built-in). That isn't present at all in prod, or when running in k8s or docker compose locally.
A few years ago, I surfaced a security bug in an integrated .env loader that partly leveraged a lib and partly was DIY/NIH code. A dev built something that would traverse up and down file hierarchies to search for .env.* files and merge them runtime and reload the app if it found a new or changed one. Useful for dev. But on prod, uploading a .env.png would end up in in a temp dir that this homebuilt monstrosity would then pick up. Yes, any internet user could inject most configuration into our production app.
Because a developer built a solution to a problem that was long solved, if only he had researched the problem a bit longer.
We "fixed" it by ripping out thousands of LOCs, a dependency (with dependencies) and putting one line back in the READMe: use an env loader like .... Turned out that not only was it a security issue, it was an inotify hogger, memory hog, and io bottleneck on boot. We could downsize some production infra afterwards.
Yes, the dev built bad software. But, again, the problem wasn't that quality, but the fact it was considered to be built in the first place.
> What problem does it solve that hasn't been solved by other people?
nothing. Except for "portability" arguments perhaps.
Java has had security managers and access restrictions built in but it never worked very well (and is quite cumbersome to use in practice). And there's been lots of bypasses over the years, and patch work fixes etc.
Tbh, the OS is the only real security you could trust, as it's as low a level as any application would typically go (unless you end up in driver/kernal space, like those anti-virus/anti-cheat/crowdstrike apps).
But platform vendors always want to NIH and make their platform slightly easier and still present the similar level of security.
How would you solve this at the OS level across Linux, macOS and Windows?
I've been trying to figure out a good way to do this for my Python projects for a couple of years now. I don't yet trust any of the solutions I've come up with - they are inconsistent with each other and feel very ironed to me making mistakes due to their inherent complexity and lack of documentation that I trust.
If something is solved at the OS level it probably needs to vary by OS. Just like how an application layer solution to parsing data must vary slightly between nodeJS and java.
For a solution to be truly generic to OS, it's likely better done at the network level. Like by putting your traffic through a proxy that only allows traffic to certain whitelisted / blacklisted destinations.
The proxy thing solved for betroth access but not for filesystem access.
With proxies the challenge becomes how to ensure the untrusted code in the programming language only accesses the network via the proxy. Outside of containers and iptables I haven't seen a way to do that.
I guess my point was that we have different OS's precisely because people want to do things in different ways. So we can't have generic ways to do them.
OS generic filesystem permissions would be like a OS generic UI framework, it's inherently very difficult and ultimately limited.
Separately, I totally sympathise with you that the OS solutions to networking and filesystem permissions are painful to work with. Even though I'm reasonably comfortable with rwx permissions, I'd never allow untrusted code on a machine which also had sensitive files on it. But I think we should fix this by coming up with better OS tooling, not by moving the problem to the app layer.
Why would a desktop program need these sort of restrictions?
Because I don't trust the developer not to have security holes in their code.
But you are asking the developer to make these restrictions... Node.js is the user-space program, controlled by developers. Ops shouldn't (need to) deal with it.
> Why reinvent this wheel, adding complexity, bug-surface, maintenance burden and whatnot to your project? What problem dies it solve that hasn't been solved by other people?
Whilst this is (effectively) an Argument From Authority, what makes you assume the Node team haven't considered this? They're famously conservative about implementing anything that adds indirection or layers. And they're very *nix focused.
I am pretty sure they've considered "I could just run this script under a different user"
(I would assume it's there because the Permissions API covers many resources and side effects, some of which would be difficult to reproduce across OSes, but I don't have the original proposal to look at and verify)
OS level checks will inevitably work differently on different OSes and different versions. Having a check like this in the app binary itself means you can have a standard implementation regardless of the OS running the app.
I often hear similar arguments for or against database level security rules. Row level security, for example, is a really powerful feature and in my opinion is worth using when you can. Using RLS doesn't mean you skip checking authorization rules at the API level though, you check on author in your business logic _and_ in the database.
OK, I'll bite. Do you think Node.js implementation is aware of DNS search path? (My guess would be that it's unaware with 90% certainty).
If you don't know what DNS search path is, here's my informal explanation: your application may request to connect to foo.bar.com or to bar.com, and if your /etc/resolv.conf contains "search foo", then these two requests are the same request.
This is an important feature of corporate networks because it allows macro administrative actions, temporary failover solutions etc. But, if a program is configured with Node.js without understanding this feature, none of these operations will be possible.
From my perspective, as someone who has to perform ops / administrative tasks, I would hate it if someone used these Node.js features. They would get in the way and cause problems because they are toys, not a real thing. Application cannot deal with DNS in a non-toy way. It's the task for the system.
Oh I'd be very surprised if Node's implementation would handle such situations.
I also wouldn't really expect it to though, that depends heavily on the environment the app is run in, and if the deployment environment intentionally includes resolv.conf or similar I'd expect the developer(s) to either use a more elegant solution or configure Node to expect those resolutions.
Then you are sort of saying: I expect Node.js implementation to be bad... why do you need a bad solution if a good one is within hand's reach?
In other words: Node.js doesn't do anything better, but, actually does some things worse. No advantages, only disadvantages... then why use it?
Putting network restrictions in the application layer also causes awkward issues for the org structures of many enterprises.
For example, the problem of "one micro service won't connect to another" was traditionally an ops / environments / SRE problem. But now the app development team has to get involved, just in case someone's used one of these new restrictions. Or those other teams need to learn about node.
This is non consensual devops being forced upon us, where everyone has to learn everything.
My experience with DevOps has been they know a lot about deploying and securing Java, or Kotlin, or Python but they know scant about node js and its tooling and often refuse to learn the ecosystem
This leads to the node js teams to have to learn DevOps anyway because the DevOps teams do a subpar job with it otherwise.
Same with doing frontend builds and such. In other languages I’ve noticed (particularly Java / Kotlin) DevOps teams maintain the build tools and configurations around it for the most part. The same has not been true for the node ecosystem, whether it’s backend or Frontend
Genuine question, as I've not invested much into understanding this. What features of the OS would enable these kinds of network restrictions? Basic googling/asking AI points me in the direction of things that seem a lot more difficult in general, unless using something like AppArmor, at which point it seems like you're not quite in OS land anymore.
How many apps do you think has properly set user and access rights only to what they need? In production? If even that percentage was high, how about developers machines, people that run some node scripts which might import whoever knows what? It is possible to have it running safely, but I doubt it's a high percentage of people. Feature like this can increase that percentage
Wouldn't "simplifying" or even awareness of the existence of such OS features be a much better solution than re-building it in a runtime?
If an existing feature is used too little, then I'm not sure if rebuilding it elsewhere is the proper solution. Unless the existing feature is in a fundamentally wrong place. Which this isn't: the OS is probably the only right place for access permissions.
An obvious solution would be education. Teach people how to use docker mounts right. How to use chroot. How Linux' chmod and chown work. Or provide modern and usable alternatives to those.
Your point about OS caring about this stuff is solid, but saying a solution is education seems a little bit naive. How are you going to teach people? Or who is going to do that? If node runtime makes its use safer by implementing this, that helps a lot of people. To say people need to learn themselves helps noone.
In Deno you can make a runtime that cannot even access the filesystem.
That's a cool feature. Using jlink for creating custom JVMs does something similar.
That's a good feature. What you are saying is still true though, using the OS for that is the way to go.
What's there to dislike? They don't replace the restrictions at OS level, they add to it.
Nope, they don't add. They confuse. From administrator perspective, it sucks when the same conceptual configuration can be performed in many different places using different configuration languages, governed by different upgrade policies, owned by unintended users, logged into unintended places.
Also, I'd bet my monthly salary on that Node.js implementation of this feature doesn't take into account multiple possible corner cases and configurations that are possible on the system level. In particular, I'd be concerned about DNS search path, which I think would be hard to get right in userspace application. Also, what happens with /etc/hosts?
From administrator perspective I don't want applications to add another (broken) level of manipulating of discovery protocol. It usually very time consuming and labor intensive task to figure out why two applications which are meant to connect aren't. If you keep randomly adding more variables to this problem, you are guaranteed to have a bad time.
If you're confused over such things, you're a crap admin.
Oh, so we are launching attacks on personality now? Well. To start with: you aren't an admin at all, and you don't even understand the work admins do. Why are you getting into an argument that is clearly above your abilities?
And, a side note: you also don't understand English all that well. "Confusion" is present in any situation that needs analysis. What's different is the degree to which it's present. Increasing confusion makes analysis more costly in terms of resources and potential for error. The "solution" offered by Node.js offers to increase confusion, but offers nothing in return. I.e. it creates waste. Or, put differently, is useless, and, by extension, harmful, because you cannot take resources and do nothing and still be neutral: if you waste resources while produce nothing of value, you limit resources to other actors who could potentially make a better use of them.
It's similar to the refrain "they shouldn't add that feature to language X, people should just use language Y instead" ("just" when said by software developers is normally a red flag IME)
The pragmatic reason is that the runtime should have more permissions than the code, eg in node require('fs') likely read files in system folders
Not necessarily, in selinux for example you would configure a domain for the "main process" which can transition into a lower permission domain for "app" code.
Path restrictions look simple, but they're very difficult to implement correctly.
PHP used to have (actually, still has) an "open_basedir" setting to restrict where a script could read or write, but people found out a number of ways to bypass that using symlinks and other shenanigans. It took a while for the devs to fix the known loopholes. Looks like node has been going through a similar process in the last couple of years.
Similarly, I won't be surprised if someone can use DNS tricks to bypass --allow-net restrictions in some way. Probably not worth a vulnerability in its own right, but it could be used as one of the steps in a targeted attack. So don't trust it too much, and always practice defense in depth!
Last time a major runtime tried implementing such restrictions on VM level, it was .NET - and it took that idea from Java, which did it only 5 years earlier.
In both Java and .NET VMs today, this entire facility is deprecated because they couldn't make it secure enough.
I believe that the various OSes have implemented appropriate syscalls such as openat to support it
e.x. https://go.dev/blog/osroot
Even that doesn't protect you from bind mounts. The rationale seems to be that only root can create bind mounts. But guess what, unprivileged users can also create all sorts of crazy mounts with fuse.
The whole idea of a hierarchical directory structure is an illusion. There can be all sorts of cross-links and even circular references.
I wouldn't trust it to be done right. It's like a bank trusting that all their customers will do the right thing. If you want MAC (as opposed to DAC), do it in the kernel like it's supposed to be; use apparmor or selinux. And both of those methods will allow you to control way more than just which files you can read / write.
Yeah but you see, this requires to be deployed along side the application somehow with the help of the ops team. While changing the command line is under control of the application developer.
So security theatre is the best option? I'm not saying this to be cheeky, but it just seems to be an overly shallow option that is trivially easy to end run.
Agreed.
How can we offer a solution that is as low or lower friction and does the right thing instead of security theater.
At least we could consider this part of a defense in depth.
We; humans; always reach for instant gratification. The path of low resistance is the one that wins.
Just because you have a safe doesn't mean the lock on the front door is useless.
> I wouldn't trust it to be done right.
I don't understand this sort of complaint. Would you prefer that they didn't worked on this support ever? Exactly what's your point? Airing trust issues?
Node allows native addons in packages via the N-API so any native module aren't restricted by those permissions. Deno deals with this via --allow-ffi but these experimental Node permissions have nothing to disable the N-API, they just restrict the Node standard library.
> Node allows native addons in packages via the N-API so any native module aren't restricted by those permissions. (...) Node permissions (...) just restrict the Node standard library.
So what? That's clearly laid out in Node's documentation.
https://nodejs.org/api/permissions.html#file-system-permissi...
What point do you think you're making?
What is the point of a permissions system that can be trivially bypassed?
> What is the point of a permissions system that can be trivially bypassed?
You seem to be confused. The system is not bypassed. The only argument you can make is that the system covers calls to node:fs, whereas some modules might not use node:fs to access the file system. You control what dependencies you run in your system, and how you design your software. If you choose to design your system in such a way that you absolutely need your Node.js app to have unrestricted access to the file systems, you have the tools to do that. If instead you want to lock down file system access, just use node:fs and flip a switch.
To check a box
> need to demonstrate security compliance.
Can't seem to find an official docs link for allow-net, only blog posts.
https://github.com/nodejs/node/pull/58517 - I think the `semver-major` and the timing mean that it might not be available until v25, around October