When you use a Node module, have you thought that maybe a dependency of a dependency that your script uses, could become malicious?
That dependency has the same access to your computer as your own code. It could look through your files then phone home anything it wants, as recently seen with crypto wallets in the event-stream attack, or sensitive Linux files. These types of attacks are called "open source supply chain attacks", and hundreds of malicious package attacks have been documented.
Deno forbids access to the filesystem by default, and you can whitelist specific directories. Node doesn't have this capability.
One solution is to run your scripts in a sandboxed environment such as firejail, bubblewrap, or a VM (Docker is a containerization solution, not aimed at security).
Once you've installed and configured sandboxing, it would help to check if your scripts are indeed sandboxed correctly, and abort immediately otherwise, so that no dependency has a chance to execute. To that purpose, I've published a module called "behind-bars", which does exactly that. All you have to do is add this line at the top of your script:
import 'behind-bars';
The module will call process.exit()
immediately if it can access sensitive files or directories (browser profiles, cryptocurrency wallets, ~/*_history etc). It does so before any other imported module has a chance to run and steal data, by only using synchronous calls. You can optionally configure it to also ensure there's no Internet access, and you can define your own custom paths to sensitive files.
So far it checks for common sensitive files on Linux and MacOS systems. PRs for Windows are welcome in the repo!
Implementation challenges
To provide the maximum level of security, the module code needs to execute before any other imported code. Thus the developer must add the import 'behind-bars'
line first at the top of the script. At that point, the module code needs to complete two types of requests before any other code runs:
- Checking the filesystem for accessible paths
- Checking for Internet access (by fetching a specified URL)
Both these requests are normally asynchronous, which means other module code will get executed before they complete. This is unacceptable. Starting with Node v14.3.0, top-level await
has been unflagged, and we could use it to wait for the requests to complete. The problem is, when transpiling to CommonJS, top-level await doesn't actually block the other imports from executing.
This leaves using synchronous requests as the only backwards-compatible option. Unfortunately this means using old libraries like glob
and sync-requests
, which pull a total of ~40+ dependencies. If dropping CommonJS compatibility were ok, we could use the far lighter tiny-glob, and node's native http
/https
module.
Top comments (2)
Bundlers/Node should have something like this by default. Cool idea 👏
Deno has this by default.