Tl; DR
Using npm, the NodeJS package manager, is a security issue. Regular means cannot control access rights granted to libraries. Together with an abundance of micromodules, this can lead to unpredictable consequences, some of what has already happened are described here , and in the best traditions of the npm ecosystem I refer to it.
Under the kat, a proof-of-concept library is described that implements a mechanism for loading npm-modules with the ability to set permissions, just like on Android you can issue specific permissions to an application.
Instead
var lib = require('untrusted-lib');
invited to write somewhere
')
var paraquire = require('paraquire')(module);
and then
var lib = paraquire('untrusted-lib');
or
var lib = paraquire('untrusted-lib', {builtin:{https:true}});
Source code is available
on githab under LGPLv3.
In addition, without being an experienced NodeJS developer, I ask the community for advice and discussion.
Brief history analysis
Currently, several attack vectors for npm and its packages are known. The most attractive packages for attacks are those that many other packages depend on.
- Package recall. In a destructive form, it was demonstrated by Azer Kochul, who withdrew the leftpad package. Currently not feasible, because Package review is very limited.
- Malicious preinstall and postinstall scripts. Currently - the main method of attack (see link above). Theoretically, it can be easily stopped by the npm install flag, which would trigger a warning before launching any shell script, so that the programmer can decide whether to run the proposed one. It's one thing when the preinstall script tries to run PhantomJS (headless browser), and quite another when the small library requires it to add spaces to the left of the line.
Why such a flag has not yet been introduced is unclear.
UPD: Thanks to SDSWanderer for commenting on the opportunity to make
npm install --ignore-scripts
However, an interactive version (preferably showing the contents of the script) would not be superfluous either.
- And, finally, you can hide the malicious load directly into the executable code of the package, as described here in a comic form. The npm shrinkwrap command, which rigidly fixes the version of the package, will not help from “time-lapse mines”, which, for example, are waiting for a certain date (recall “Chernobyl”). In addition, with the current abundance of dependencies, to audit the source code of even one version of the required package, along with everything that this package uses - the task is, of course, feasible, but obviously not everyday.
To combat the last vector, the paraquire library was created. The code of untrusted libraries is isolated in separate
execution contexts . The
internal means of the NodeJS itself are actively used to manage the modules. In other words, an alternative module management system was created, the possibility of which is clearly indicated by the NodeJS developers themselves.
Using
Currently, the API is rather poor.
So, first, let's load the paraquire library itself somewhere in relatively trusted code, i.e. code of our application:
var paraquire = require('paraquire')(module);
Notice the "(module)". Since paraquire is involved in managing modules, it needs to know which module it is necessary to connect dependencies to, so the connection of paraquire looks somewhat unusual. You can connect paraquire to any number of modules from your project.
Let's connect now dependencies. For example, suppose that untrusted-lib is a library which, in theory, should not require anything (for example,
leftpad or
imurmurhash ). Then we can not give it access to anything at all by connecting it like this:
var lib = paraquire('untrusted-lib');
Everything, attempt to make require ('fs') in the code untrusted-lib will cause an error.
The paraquire function has a second, optional, object parameter. For example, this is how you can provide the untrusted-lib library with access to the http and https embedded modules, as well as to the console and process.argv console:
var lib = paraquire('untrusted-lib', { builtin: { http: true, https: true, }, sandbox: { console: console, process: {argv:process.argv} }, });
By the way, giving access to the entire process is strongly discouraged, in particular, because of process.env and process.bindings ('fs').
I ask for advice
As mentioned above, I am not a sufficiently experienced developer, and therefore this brief publication is intended to initiate discussion as soon as possible. Is this topic relevant? How are similar problems solved in other languages, in particular, in Ruby and Python? Maybe someone can suggest a way around paraquire and get elevated privileges?
In which direction should the API be developed? Maybe it is worth to consult someone from foreign masters? What questions should be covered in further articles and is it worth writing them at all?