Policy is one of the less frequently talked about features in NodeJS. The purpose of this feature is to enforce a level of security to what kind of code is loadable into your NodeJS Application (Which is kind of similar to deno --allow-<module>
but more multifaceted).
Policies are currently experimental
and can be used with the --experimental-policy
flag
This feature was introduced recently, and may change
or be removed in future versions. Please try it out and provide feedback. If it addresses a use-case that is important to you, tell the node core team.
Every loadable code will go through an integrity verification check by comparing the sha256 value (base64 encoded) with what was specified in relation to that resource as well as all subresources. If there is a mismatch of the sha256 value with what was specified in the policy manifest file (this file dictates how a code should or should not be loaded), the behavior of what happens next will be defined in the policy manifest file.
The sha256 value is calculated from the content of the loadable resource.
For example, if we have this code
console.log('test')
copy the above into an empty folder and name it test.js
To get the sha256 value of test.js
, you can use the oneliner specified in the node documentation for policies
node -e 'process.stdout.write("sha256-");process.stdin.pipe(crypto.createHash("sha256").setEncoding("base64")).pipe(process.stdout)' < ./test.js
{
"onerror": "log",
"resources": {
"./test.js": {
"integrity": "sha256-LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="
}
}
}
copy the above into the same folder as test.js
and name it policy.json
onerror
can either be log
, throw
, or exit
. When the integrity check fails, log
outputs the error and continues execution of your program.
The loadable resources in this context is test.js
. When onerror
is not specified the default value is throw
, it logs out the error and does not continue executing your program.
Running the below command will output a bunch of ERR_MANIFEST_ASSERT_INTEGRITY
as well as test
.
node --experimental-policy=./policy.json ./test.js
not specifying the integrity field, defaults to
null
specifyingtrue
as the value for integrity bypasses the verification check. Any loadable resource that is loaded using eitherrequire
orimport
is also subject to the integrity verification check. The sha256 value becomes invalid when a little change is made to any of the loadable resource
Change the onerror
value from log to either throw
or exit
to see how it behaves when a wrong sha256 value is used for a resource
Enabling/Disabling modules from been loaded
copy the below code into test-2.js
const fs = require("node:fs");
const os = require("node:os");
const test2_1 = require("./test-2-1.js");
console.log(fs.statSync(__filename));
console.log(os.userInfo());
copy the below code into test-2-1.js
const net = require("node:net");
console.log(new net.SocketAddress());
Run the below oneliner to generate the sha256 value for integrity verification.
node -e 'process.stdout.write("sha256-");process.stdin.pipe(crypto.createHash("sha256").setEncoding("base64")).pipe(process.stdout)' < ./test-2.js
node -e 'process.stdout.write("sha256-");process.stdin.pipe(crypto.createHash("sha256").setEncoding("base64")).pipe(process.stdout)' < ./test-2-1.js
copy the below manifest into policy-2.json
{
"onerror": "log",
"resources": {
"./test-2.js": {
"integrity": "sha256-input test-2.js base64 encoded hash here",
"dependencies": {
"node:fs": true,
"node:os": true,
"./test-2-1.js": true
}
},
"./test-2-1.js": {
"integrity": "sha256-input test-2-1.js base64 encoded hash here",
"dependencies": {
"node:net": true
}
}
}
}
The dependencies
field contains the list of dependencies (used in a resource or subresource) and the rules of how it should be loaded. Subresource are resources that are loaded by other resource, for example test-2-1.js
is a subresource to test-2.js
Run
node --experimental-policy=./policy-2.json ./test-2.js
The output will be something like this , depending on your computer
SocketAddress { address: '127.0.0.1', port: 0, family: 'ipv4', flowlabel: 0 }
Stats {
dev: 16777221,
mode: 33188,
nlink: 1,
uid: 502,
gid: 20,
rdev: 0,
blksize: 4096,
ino: 15164992,
size: 170,
blocks: 8,
atimeMs: 1645483771373.328,
mtimeMs: 1645483770300.6633,
ctimeMs: 1645483770300.6633,
birthtimeMs: 1645482935166.657,
atime: 2022-02-21T22:49:31.373Z,
mtime: 2022-02-21T22:49:30.301Z,
ctime: 2022-02-21T22:49:30.301Z,
birthtime: 2022-02-21T22:35:35.167Z
}
{
uid: 502,
gid: 20,
username: 'victoryosikwemhe',
homedir: '/Users/victoryosikwemhe',
shell: '/usr/local/bin/bash'
}
policy-two.json
manifest file enables every dependency required/imported in ./test-2-1.js
and ./test-2.js
, a dependency can be disabled by setting the value of the dependency to false
{
"onerror": "log",
"resources": {
"./test-2.js": {
"integrity": "sha256-input test-2.js base64 encoded hash here",
"dependencies": {
"node:fs": true,
"node:os": true,
"./test-2-1.js": true
}
},
"./test-2-1.js": {
"integrity": "sha256-input test-2-1.js base64 encoded hash here",
"dependencies": {
"node:net": false
}
}
}
}
setting node:net
to false
disables the node core net
module in only test-2-1.js
, when test-1.js
tries loading test-2-1.js
it will cause an error.
Run
node --experimental-policy=./policy-2.json ./test-2.js
It will throw ERR_MANIFEST_INVALID_RESOURCE_FIELD(href, 'dependencies')
on test-2-1.js
Enforcing using import
You can also enforce that a module should be loaded with import
or require
Modify test-2.js
and test-2-1.js
respectively to look like the below (You will have to generate the sha256 value of the contents)
test-2.js
const { syncBuiltinESMExports } = require("node:module");
const os = require("node:os");
const test2_1 = require("./test-2-1.js");
console.log(os.userInfo());
syncBuiltinESMExports();
import("node:fs").then( f => {
console.log(f.statSync(__filename));
});
test-2-1.js
const net = require("node:net");
console.log(new net.SocketAddress());
module.exports = {};
(Note: Generate a new sha254 value for the above resources, you can also set integrity to true to avoid doing this for every little change - even for a single space)
{
"onerror": "log",
"resources": {
"./test-2.js": {
"integrity": true,
"dependencies": {
"node:fs": { "require": true },
"node:os": { "import": true },
"node:module": true
"./test-2-1.js": true
}
},
"./test-2-1.js": {
"integrity": true,
"dependencies": {
"node:net": true
}
}
}
}
Run
node --experimental-policy=./policy-2.json ./test-2.js
This will throw ERR_INVALID_URL
because ./test-2.js
should only load node:fs
with esm import
. Changing require: true
to import: true
or loading node:fs
with cjs require
will make this check go away.
Sadly, switching the flip to module.createRequire
behaves differently.
Loading a different module other than what is required/imported
Another form of dependency redirection is loading Module A when Module B was initially required/imported.
test-3.js
const fs = require('node:fs');
console.log(nodeFetch);
fs.readFileSync(__filename);
mocked-fs.js
module.exports = {
readFileSync(location) {
console.log({ location });
}
}
policy-3.json
{
"onerror": "log",
"resources": {
"./package.json": {
"integrity": true
},
"./test-3.js": {
"integrity": true,
"dependencies": {
"node:fs": "./mocked-fs.js"
}
},
"./mocked-fs.js": {
"integrity": true
}
}
}
Run
node --experimental-policy=./policy-3.json ./test-3.js
Output
{ location: '/Users/victoryosikwemhe/pp/test-3.js' }`
Instead of loading the fs
module , it redirects to mocked-fs.js
The policy manifest file also supports scopes
, import maps
and cascading
. I will cover them in the next part, until then, you can checkout the documentation on policies
Top comments (1)