Yarn Plug'n'Play (.pnp.cjs)
On every Node process that loads it (yarn node, yarn run, or explicit --require)
- Lives in
.pnp.cjs in the project root- Attacker needs
- Supply-chain foothold
- Lifecycle
- InstallStartupyour codeModule loadRuntimeREPL
Runs on import or require, for every module the app loads.
How it runs
Yarn Berry (v2+) replaces node_modules with Plug'n'Play. A generated .pnp.cjs file acts as a custom module resolver by patching Node's Module._resolveFilename. In effect it's a universal preload that controls all module resolution for the process.
.pnp.cjs is JavaScript. It's checked into the repo, as Yarn recommends, and loaded on every Node invocation through Yarn tooling. It's usually thousands of lines of generated code, so a one-line backdoor blends into what looks like a normal regen diff.
node --require ./.pnp.cjs app.js # yarn node / yarn run add this automatically
Two areas of a diff matter for security. Near the top, the RAW_RUNTIME_STATE blob lists each package's packageLocation and packageDependencies. Repoint a location at an attacker directory and you've swapped what resolves without touching node_modules in the classic way. Further down, Yarn wraps _resolveFilename. A malicious diff can add an early return for specific requests and bypass the registry entirely. And because .pnp.cjs is executable JavaScript, any statement in it runs the moment Node loads the shim, before your entrypoint.
Defend
Review .pnp.cjs diffs beyond the boilerplate. Watch packageLocation, packageDependencies, and _resolveFilename patches; regenerate from trusted installs only.