DEV Community

Victory Osikwemhe
Victory Osikwemhe

Posted on • Updated on

Policies in NodeJS - Part 1

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')
Enter fullscreen mode Exit fullscreen mode

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="
      }
   }
}
Enter fullscreen mode Exit fullscreen mode

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 specifying true as the value for integrity bypasses the verification check. Any loadable resource that is loaded using either require or import 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());
Enter fullscreen mode Exit fullscreen mode

copy the below code into test-2-1.js

const net = require("node:net");
console.log(new net.SocketAddress());
Enter fullscreen mode Exit fullscreen mode

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
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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'
}
Enter fullscreen mode Exit fullscreen mode

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
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
});

Enter fullscreen mode Exit fullscreen mode

test-2-1.js

const net = require("node:net");
console.log(new net.SocketAddress());
module.exports = {};
Enter fullscreen mode Exit fullscreen mode

(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
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

mocked-fs.js

module.exports = {
  readFileSync(location) {
    console.log({ location });
  }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Run
node --experimental-policy=./policy-3.json ./test-3.js

Output

{ location: '/Users/victoryosikwemhe/pp/test-3.js' }`
Enter fullscreen mode Exit fullscreen mode

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

Discussion (1)

Collapse
Sloan, the sloth mascot
Comment deleted