node-module-confinement v2.1.0
node-module-confinement
This package provides a simple way for confining modules and preventing them to load unwanted other modules, or redirect modules to load to different ones.
You can set up a blacklist and a whitelist for this confinement. You can tell the confinement to prevent node
internal modules completely, only allowing a few ones by whitelist. Additionally, you can redirect require calls
to your own modules, like redirecting fs to safe-fs, which provides a trusted subset of fs-access.
If anything defies the confinement, an error is thrown to prevent anything bad from happening. The error tells you which confinement was defied and which module did this.
On top of all that, this module comes with some "addons", which allow you to setup some traps to prevent unwanted code execution methods.
Why
Using dependencies is dangerous. Installing simple npm-packages might download tons of other unknown dependencies, which you can't review all. If only one of such dependencies contain malicious code, you're screwed.
This module helps to reduce the attack-surface, as you can limit access to node internal modules or critical configuration files. That way bad code leads to an error, preventing bad stuff from happening.
So this module helps with preventing supply-chain-attacks or dependency-attacks, and helps to secure your application by simply enforcing policies.
API
The main API exists of only a single function: setup. The signature is: setup(moduleContext, confinementConfiguration)
The moduleContext is the current module value. This is used to resolve all modules referenced by the
confinementConfiguration.
The confinementConfiguration is the most important part, and looks something like this:
const confinementConfiguration = {
defaultConfinement: {
allowBuiltIns: true
blackList: [],
whiteList: [],
redirect: {}
},
confinements: {
'./some/module.js': {
applyToChildren: false,
allowBuiltIns: false
blackList: [],
whiteList: ['fs', 'path'],
redirect: {}
},
'./some/other/module.js': {
applyToChildren: false,
allowBuiltIns: false
blackList: [],
whiteList: [],
redirect: {}
}
},
addons: {
trapEval: false,
trapFunction: false
}
};Confinements
So, bascially a confinement consists of 4 main rules: allowBuiltIns, whiteList, blackList and redirect. The rules are applied as follows:
First of let's look at whiteList and blackList, as they are the most straight forward. whiteListed modules are allowed by
default, blackListed modules are denied by default. All modules passed in this arrays get resolved to their actual module-filename.
To do so all modules get resolved relative to given moduleContext (which was passed as first parameter to setup-function).
The second part to consider is the allowBuiltIns flag, which tells to allow or deny all builtin nodejs modules. So if you put allowBuiltIns=false
calling stuff like require('fs') will throw an error. This is basically a shortcut to add all node builtin modules to the blackList. To allow
loading at least some builtin modules, you can pass them to the whiteList, so you can write a confinement like {allowBuiltIns: false, whiteList: ['fs']}
to allow require('fs'), but deny all other builtins like require('path').
After applying all the rules above, we know whether the current require call is allowed or forbidden. If it's allowed, the last property comes into
play: redirect. This contains a map of module -> module contents. So if the module wants to load a certain other module, you can redirect the call to
load yet another module. This is useful for example when redirecting builtins to mocks or limited variants, something like:
const confinementConfiguration {
defaultConfinement: {
allowBuiltIns: false,
whiteList: ['fs'],
redirect: {
'fs': './myfsmock.js'
}
}
}That way we can redirect require calls to different modules for security purpose (or whatever).
The last option of a confinement is applyToChildren, which is a flag only applied during confinement evaluation. To know understand
this flag, you have to understand how confinements get resolved:
- First the module, that should be loaded, gets resolved to its module-filename
- Then we check, whether the resolved module-filename matches with any
confinements-entry. If so, we use this confinement - Because we have no specific confinement, we traverse the module-tree upwards, to check whether any parent has a confinement.
If any parent has a confinement, we check the
applyToChildrenflag. If it's true, we use this confinement. If it's false, we traverse the tree upwards up to the root module (providedmoduleContext) - If we couldn't find any confinement up till now, we use the
defaultConfinement - If the
defaultConfinementwas empty, no confinement is applied
This should make it obvious, what applyToChildren does.
This should explain everything about confinements, that you need to know.
Addons
Another special topic is "addons". This module contains some addons, that help with securing your application. You can apply the
following addons by simply setting them to true in the configuration.
trapEval
This is pretty obvious: Proxy the global eval function and throw an error each time it gets called. This way eval doesn't work
anymore, and malicious eval-calls are prevented.
trapFunction
This is basically as obvious as the previous one, but most people don't know about this one: The function constructor is basically
a hidden eval function. Stuff like Function('console.log(process.pid);')() might not do bad, but does the same as
eval('console.log(process.pid);'), so its basically eval in disguise.
To prevent usage of this hidden eval-like stuff, you can apply a trap by enabling this option. This trap will prevent all Function(...)
and new Function(...) calls and throw an error each time it happens.