Sandboxing npm packages


(Lars Willighagen) #1

A static safety-check for npm might not work, but – hear me out – a dynamic runtime one could. Perhaps. The idea may be a bit of a stretch, the implementation definitely is, and the usefulness is limited (as is the concept in and of itself). However, it could work, and I can’t have this idea sitting in the back of my head any longer.

The Idea

Permissions are applied through the scope of a set of files, which is usually limited to the root directory of a package and downwards. Exceptions are node_modules, which always has zero permissions, and possibly bin files. Each package in node_modules has it’s own set of permissions, although those could be inherited from parent packages (see “Usage”).

The Implementation

This is were tink comes in. See, tink implements its own require function. I think. If it doesn’t, @babel/register does, so it’s definitely possible. Anyway, if you redefine the require function you can implement some proxy logic in there, that keeps track of the permissions of each scope. Then, by grabbing the filename of the require caller (which is a bit awkward, I know), you can determine the scope, and what permissions it has. Of course, requireing a file or module that falls outside those permissions is not allowed, and a PermissionError can be thrown.

This beats any static analysis, as this should be pretty obfuscation-proof, and safe to edge cases if used with require.resolve(). To avoid workarounds, file-system and child_process acces would be limited too, as well as possibly process and other global variables, as they may lead to packages via constructors or prototypes.

Other problems include the fact that, of course, sensitive modules can be passed in both directions. I only realized this after writing this entire post. The only workaround of this I can think of is simply expecting people not to do that, for their own sake. So that probably invalidates my entire idea. Great.

The only thing I can remember that comes close to solving this is the sandbox of https://happening.im/, which, if I remember correctly, prevented passing around modules anywhere. However, that part of their code isn’t open source, the project is discontinued, the developers are gone, and I have no idea how they did it.

The Usage

Now comes the hard part: when should permissions be given? Packages can’t simply declare permissions themselves for a number of reasons:

  • it would have to be opt-in, kind of defeating the purpose
  • being opt-in, it wouldn’t stop truly malicious people
  • etc.

On the other hand, you can’t require (pun intended) users to sift through every dependency in their project to manually assign permissions to everything. A middle ground, of having users assign permissions to top-level dependencies could work, but could also be the worst of both: giving to much permissions to sub-dependencies, and a trial-and-error process for the user. One good thing: in contrast too static checks, permissions aren’t required for optional packages that aren’t actually used.

The Use Cases

The use cases are pretty limited too, as mentioned. Proper safe-keeping of malicious packages is somewhat doable with packages that don’t need any access. However, packages that legitimately need access to say, the filesystem, they can probably work around the imposed limitations.


I don’t think this is worthy of an actual RFC, as it’s not even slightly realistic, and probably not something for npm anyway, so I just posted it here.