NODE_OPTIONS preload injection
Before the main script runs (--require, --import) or during module loading (--experimental-loader)
- Lives in
NODE_OPTIONS environment variable, CLI flags, or .env files loaded via --env-file- Attacker needs
- Environment control
- Lifecycle
- InstallStartupyour codeModule loadRuntimeREPL
Runs while the interpreter boots, before your entrypoint.
How it runs
NODE_OPTIONS injects CLI flags into every Node.js process. Set it once and every node invocation on that machine, container, or CI pipeline picks it up. It's the most powerful way to run code implicitly in Node, because it carries several flags that each run code before main.
--require preloads a CommonJS (or, on modern Node, ESM) module before your entrypoint:
export NODE_OPTIONS="--require /tmp/demo.js"
node app.js # /tmp/demo.js already ran
--import is the dedicated ESM preload flag. --experimental-loader installs a loader hook that intercepts, rewrites, or redirects any import. That makes it a man-in-the-middle for the module system. It can replace fs with a patched version that exfiltrates reads, or inject logging into every import.
--env-file (Node 20.6+) loads env vars from a file before the process initializes. If a .env contains NODE_OPTIONS, the whole preload chain moves out of the environment and into a file most developers consider harmless config:
# .env
NODE_OPTIONS=--require /tmp/evil.js
All of these flow through one variable, and an attacker can chain them, as in NODE_OPTIONS="--require ./cjs-hook.js --import ./esm-hook.mjs --experimental-loader ./loader.mjs". If they can set env vars in your pipeline, Docker image, or .env file, they own every Node process that reads them.
Defend
Scrub NODE_OPTIONS in CI/CD, containers, and service managers; treat .env as code; avoid --env-file in package.json scripts you don't control.