Back to Intelligence

Malicious npm Packages Evade Install-Script Defenses at Runtime: Detection and Hardening Guide

SA
Security Arsenal Team
September 20, 2026
12 min read

A live, ongoing malicious software campaign on the npm registry is demonstrating an evolution in supply-chain tradecraft that defenders need to internalize immediately: threat actors are abandoning preinstall/postinstall script abuse — the classic hook that most enterprise defenses are built to catch — and instead embedding malicious logic directly in a package's normal runtime code. The package at the center of the campaign, indexed-btree, poses as a benign data-structure library. Its payload only fires when a developer's application (or a downstream CI job, test runner, or production service) actually imports and executes the module.

This matters because the dominant defensive posture against npm supply-chain attacks over the past several years has been install-script suppression: --ignore-scripts, npm's default behavior changes, sandboxed installs, and CI policies that flag any package.json containing lifecycle hooks. Those controls remain valuable — but this campaign proves they are no longer sufficient. If your dependency security strategy starts and ends with blocking install scripts, you have a detection gap that adversaries are actively walking through right now.

Technical Analysis

What the campaign does

Traditional malicious npm packages execute their payload during npm install via lifecycle scripts defined in package.json (preinstall, install, postinstall). Defenders responded en masse: organizations run installs with --ignore-scripts, lock down CI runners, and alert on any dependency carrying install hooks. Attackers adapted.

The indexed-btree campaign inverts the delivery mechanism:

  1. Publication of a plausible package. The package is published with a name and functionality that appears legitimate (a B-tree data-structure implementation indexed for lookup — the kind of utility library that attracts organic installs through search and typosquatting adjacency).
  2. No suspicious install hooks. The package.json is clean. Static scans keyed on lifecycle scripts find nothing. --ignore-scripts changes nothing because there are no scripts to ignore.
  3. Payload embedded in module code. The malicious logic lives inside the package's JavaScript source — often in the module's entry point or an initialization path — and executes the moment require('indexed-btree') or import resolves, i.e., when the host application runs.
  4. Runtime execution context. This is the critical shift: the payload now executes with the full privileges of the running application — which in a Node.js context frequently means access to environment variables (process.env), where CI/CD tokens, cloud credentials, npm tokens, database connection strings, and API keys live. It can also read the filesystem, spawn child processes via child_process, and make outbound network connections for exfiltration or second-stage retrieval.

Why this defeats common controls

ControlWhy it fails here
--ignore-scriptsNo install scripts exist to suppress
Static scan for preinstall/postinstall hookspackage.json is clean
Lockfile pinning alonePrevents surprise updates, not a malicious first install of a typosquatted/masquerading package
Install-time sandboxing (npm ci in a container)Payload never fires at install time; it fires in prod/test where credentials are richer

Worse, runtime execution means the payload detonates in production and CI environments — precisely where process.env contains the highest-value secrets. An install-time payload in a locked-down build container often finds nothing worth stealing. A runtime payload inside your deployed service finds everything.

Typical runtime behavioral indicators

While exact payloads vary, runtime npm malware in this class reliably exhibits a small set of observable behaviors from the Node.js process:

  • Reading process.env en masse and initiating outbound HTTPS connections to non-standard domains shortly after module load
  • Spawning child processes (child_process.exec, spawn) — curl, bash, PowerShell, or systeminfo-type reconnaissance
  • Reading credential-bearing files: ~/.npmrc, ~/.aws/credentials, ~/.config/gcloud, .env files in the project root
  • Writing payloads or staged artifacts to temp directories
  • Outbound beaconing from node processes that have no business making external network calls (e.g., a test runner or a server-side rendering worker)

Exploitation status

This is a confirmed, ongoing campaign — packages in this family are live in the wild and being pulled into real dependency trees. No CVE identifier has been assigned (malicious-package campaigns typically do not receive CVEs; remediation is package removal and lockfile/registry hygiene, not patching a vulnerability in legitimate code). Treat any presence of indexed-btree or unreviewed lookalike packages in your dependency tree as an incident until proven otherwise.

Detection & Response

The highest-fidelity detections for this campaign are behavioral, not signature-based: watch the node process for child process spawning, suspicious network egress, and credential-file access. The rules below are tuned to fire on what runtime npm malware does, not what it is named.

YAML
---
title: Node.js Process Spawning Shell or Reconnaissance Commands
id: 8c2e4a71-3f5b-4c9d-b6e1-7a2f3d4c5b6a
status: experimental
description: Detects node processes spawning shells, download cradles, or reconnaissance commands — a hallmark of runtime npm supply-chain payloads executing after module import.
references:
  - https://www.bleepingcomputer.com/news/security/malicious-npm-packages-evade-install-script-defenses-at-runtime/
  - https://attack.mitre.org/techniques/T1195/002/
  - https://attack.mitre.org/techniques/T1059/007/
author: Security Arsenal
date: 2026/04/06
tags:
  - attack.execution
  - attack.t1195.002
  - attack.t1059.007
logsource:
  category: process_creation
  product: windows
detection:
  selection_parent:
    ParentImage|endswith:
      - '\node.exe'
      - '\npm.exe'
      - '\npm.cmd'
      - '\yarn.cmd'
      - '\pnpm.exe'
  selection_child:
    Image|endswith:
      - '\cmd.exe'
      - '\powershell.exe'
      - '\pwsh.exe'
      - '\curl.exe'
      - '\wscript.exe'
      - '\cscript.exe'
      - '\mshta.exe'
  condition: selection_parent and selection_child
falsepositives:
  - Build tooling that legitimately shells out (node-gyp, some test harnesses) — tune by build host
level: high
---
title: Node.js Process Accessing Credential Files
id: 3d7b1f42-8a9c-4e2d-9f5b-6c1a2e3d4b5c
status: experimental
description: Detects node processes reading credential-bearing files such as .npmrc, AWS credentials, or .env files — consistent with secret harvesting by malicious npm packages at runtime.
references:
  - https://www.bleepingcomputer.com/news/security/malicious-npm-packages-evade-install-script-defenses-at-runtime/
  - https://attack.mitre.org/techniques/T1552/001/
author: Security Arsenal
date: 2026/04/06
tags:
  - attack.credential_access
  - attack.t1552.001
  - attack.t1195.002
logsource:
  category: file_event
  product: windows
detection:
  selection_image:
    Image|endswith:
      - '\node.exe'
      - '\npm.exe'
  selection_target:
    TargetFilename|contains:
      - '\.npmrc'
      - '\.aws\credentials'
      - '\.config\gcloud\'
      - '\.ssh\id_'
    TargetFilename|endswith:
      - '\.env'
      - '\.env.local'
      - '\.env.production'
  condition: selection_image and selection_target
falsepositives:
  - Legitimate build/deploy scripts reading .env configuration — baseline per build agent and alert on outliers
level: high
---
title: Node.js Outbound Network Connection to Rare External Destination
id: 5e1c9d83-2b4f-4a6e-8c7d-9e0f1a2b3c4d
status: experimental
description: Detects node processes establishing outbound connections to non-registry destinations. Runtime npm payloads frequently beacon or exfiltrate environment variables shortly after module load.
references:
  - https://www.bleepingcomputer.com/news/security/malicious-npm-packages-evade-install-script-defenses-at-runtime/
  - https://attack.mitre.org/techniques/T1071/001/
  - https://attack.mitre.org/techniques/T1195/002/
author: Security Arsenal
date: 2026/04/06
tags:
  - attack.command_and_control
  - attack.exfiltration
  - attack.t1071.001
logsource:
  category: network_connection
  product: windows
detection:
  selection:
    Image|endswith:
      - '\node.exe'
      - '\npm.exe'
      - '\npm.cmd'
    Initiated: 'true'
  filter_registries:
    DestinationHostname|contains:
      - 'npmjs.org'
      - 'npmjs.com'
      - 'yarnpkg.com'
      - 'github.com'
      - 'registry.npmmirror.com'
  filter_local:
    DestinationIp|startswith:
      - '10.'
      - '172.16.'
      - '192.168.'
      - '127.'
  condition: selection and not filter_registries and not filter_local
falsepositives:
  - Applications legitimately calling external APIs — this rule is best deployed on CI/CD runners and build agents, where node should rarely phone arbitrary hosts
level: medium
KQL — Microsoft Sentinel / Defender
// Hunt: Node/npm processes spawning shells or download tools across Windows and Linux endpoints
// Covers runtime npm supply-chain payload execution after module import.
// Deploy broadly; tune KnownGoodParents per build fleet.
let KnownGoodParents = dynamic(["node", "npm", "npm.cmd", "yarn", "pnpm", "node.exe"]);
union isfuzzy=true
(DeviceProcessEvents
 | where InitiatingProcessFileName in~ (KnownGoodParents)
 | where FileName in~ ("cmd.exe","powershell.exe","pwsh.exe","curl.exe","wscript.exe","mshta.exe","bash","sh","curl","wget")
 | project Timestamp, DeviceName, InitiatingProcessFileName, InitiatingProcessCommandLine, FileName, ProcessCommandLine, AccountName),
(SecurityEvent
 | where EventID == 4688
 | where ParentProcessName has_any ("node.exe","npm.exe","npm.cmd")
 | where NewProcessName has_any ("cmd.exe","powershell.exe","pwsh.exe","curl.exe","mshta.exe")
 | project TimeGenerated, Computer, ParentProcessName, CommandLine, NewProcessName, Account),
(Syslog
 | where ProcessName =~ "node"
 | where SyslogMessage has_any ("child_process","/bin/sh","/bin/bash","curl ","wget ")
 | project TimeGenerated, Computer, ProcessName, SyslogMessage)
| order by Timestamp desc
VQL — Velociraptor
// Hunt for runtime npm supply-chain execution: node processes with suspicious
// command lines, spawned children, and access to credential files.
// Deploy across developer workstations and CI/CD runners.
SELECT Pid, Ppid, Name, Exe, CommandLine, Username, CreateTime
FROM pslist()
WHERE (Name =~ '(?i)node|npm|yarn|pnpm')
  AND (
    CommandLine =~ '(?i)child_process|curl |wget |powershell|/bin/sh|/bin/bash'
    OR CommandLine =~ '(?i)\.npmrc|\.aws/credentials|\.env'
  )

// Correlate: node processes holding open handles to credential material
SELECT Pid, Name, CommandLine, Handle.Name AS OpenFile
FROM handles()
WHERE Name =~ '(?i)node'
  AND Handle.Name =~ '(?i)\.npmrc|\.aws..credentials|id_rsa|\.env$'
Bash / Shell
#!/bin/bash
# npm supply-chain hygiene audit: find indexed-btree and suspicious runtime
# behaviors across repositories, lockfiles, and installed node_modules trees.
# Run from your monorepo root, CI checkout, or developer fleet via your EDR/MDM.

set -euo pipefail

echo "=== [1/4] Scanning lockfiles for known-malicious / unreviewed packages ==="
find . -name "package-lock.json" -o -name "yarn.lock" -o -name "pnpm-lock.yaml" 2>/dev/null | \
while read -r lockfile; do
  if grep -qiE 'indexed-btree' "$lockfile"; then
    echo "[!] MALICIOUS PACKAGE FOUND in $lockfile — treat as incident, rotate all secrets reachable from that environment"
  fi
done

echo "=== [2/4] Checking installed node_modules ==="
find . -type d -name "indexed-btree" -path "*/node_modules/*" 2>/dev/null | while read -r d; do
  echo "[!] Malicious package installed at: $d"
done

echo "=== [3/4] Auditing dependencies for unexpected postinstall hooks (defense-in-depth) ==="
find . -name "package.json" -path "*/node_modules/*" 2>/dev/null | \
  xargs grep -lE '"(preinstall|postinstall|install)"' 2>/dev/null | head -50 || true

echo "=== [4/4] Verifying npm client hardening defaults ==="
npm config get ignore-scripts
echo "Recommendation: set 'ignore-scripts=true' globally AND add runtime EDR coverage —"
echo "install-script suppression alone does NOT stop this campaign."

echo ""
echo "Done. Any [!] finding = open an IR ticket, revoke npm/cloud/CI tokens reachable"
echo "from the affected host or pipeline, and rebuild the environment from clean state."
PowerShell
# Windows developer-fleet audit for the indexed-btree npm campaign and
# suspicious node runtime behavior. Deploy via Intune, GPO startup script,
# or your RMM. Output is designed for central collection.

$ErrorActionPreference = 'SilentlyContinue'
$report = @()

# 1) Search user profiles and common repo roots for the malicious package
$searchRoots = @("$env:USERPROFILE\source", "$env:USERPROFILE\repos", "$env:USERPROFILE\dev", "C:\projects", "C:\dev")
foreach ($root in $searchRoots) {
    if (Test-Path $root) {
        Get-ChildItem -Path $root -Recurse -Filter "package-lock.json" -ErrorAction SilentlyContinue | ForEach-Object {
            if (Select-String -Path $_.FullName -Pattern 'indexed-btree' -Quiet) {
                $report += [PSCustomObject]@{
                    Severity = 'CRITICAL'
                    Finding  = "Malicious package 'indexed-btree' referenced in lockfile"
                    Path     = $_.FullName
                    Host     = $env:COMPUTERNAME
                }
            }
        }
        Get-ChildItem -Path $root -Recurse -Directory -Filter "indexed-btree" -ErrorAction SilentlyContinue |
            Where-Object { $_.FullName -like "*node_modules*" } | ForEach-Object {
                $report += [PSCustomObject]@{
                    Severity = 'CRITICAL'
                    Finding  = "Malicious package installed on disk"
                    Path     = $_.FullName
                    Host     = $env:COMPUTERNAME
                }
            }
    }
}

# 2) Check npm client hardening
$ignoreScripts = (& npm config get ignore-scripts 2>$null)
if ($ignoreScripts -ne 'true') {
    $report += [PSCustomObject]@{
        Severity = 'MEDIUM'
        Finding  = "npm ignore-scripts is not enabled (defense-in-depth gap)"
        Path     = 'npm config'
        Host     = $env:COMPUTERNAME
    }
}

# 3) Flag node processes currently spawning shells (runtime payload behavior)
Get-CimInstance Win32_Process -Filter "Name='cmd.exe' OR Name='powershell.exe'" |
    Where-Object {
        $parent = (Get-Process -Id $_.ParentProcessId -ErrorAction SilentlyContinue).ProcessName
        $parent -match '^(node|npm)$'
    } | ForEach-Object {
        $report += [PSCustomObject]@{
            Severity = 'HIGH'
            Finding  = "node process spawned shell: $($_.CommandLine)"
            Path     = $_.ExecutablePath
            Host     = $env:COMPUTERNAME
        }
    }

$report | Format-Table -AutoSize
if ($report.Severity -contains 'CRITICAL') {
    Write-Output "ACTION REQUIRED: Rotate all credentials reachable from this host (npm tokens, cloud keys, .env secrets) and rebuild from a clean image."
}

Remediation

There is no patch for a malicious package — remediation is removal, credential rotation, and pipeline hardening. Execute in this order:

  1. Identify exposure immediately. Search every lockfile (package-lock.json, yarn.lock, pnpm-lock.yaml) and installed node_modules tree across developer workstations, CI runners, container images, and deployed services for indexed-btree and any dependency you cannot attribute to a known, reputable maintainer. Include historical container images — a removed package in a still-deployed image is still an incident.

  2. Rotate secrets anywhere the package executed. Runtime npm malware harvests process.env. If the package ran in CI/CD, treat every secret available to that pipeline as compromised: npm publish tokens, cloud provider keys (AWS/GCP/Azure), registry credentials, database connection strings, SSH keys, and signing keys. Rotate all of them — do not selectively guess which were read. Revoke and reissue rather than assuming scope.

  3. Remove and rebuild. Delete the package, regenerate lockfiles from a clean state, and rebuild affected environments. On developer machines and runners where the package executed, prefer reimaging over in-place cleanup — you cannot reliably enumerate what a runtime payload with full user privileges touched.

  4. Harden the dependency pipeline (structural fixes):

    • Keep ignore-scripts=true — it's still correct — but stop treating it as a complete control.
    • Enforce a vetting gate for new dependencies: require review of any net-new package (low download counts, recent publish dates, single-maintainer packages are red flags) before it enters the lockfile. Private registry proxies (e.g., an internal npm proxy with allowlisting) make this enforceable.
    • Pin exact versions and commit lockfiles; enable npm ci in pipelines so lockfile drift fails the build.
    • Apply a cooldown period for newly published package versions — don't auto-adopt releases younger than a set threshold, since malicious versions are typically pulled from the registry within days.
    • Minimize secrets in CI environments: use short-lived OIDC-based credentials instead of long-lived tokens wherever your cloud provider supports it, shrinking the blast radius of any process.env harvest.
  5. Add runtime egress controls. The payload needs network egress to exfiltrate. Constrain outbound traffic from build runners and production Node.js services to known destinations (package registries, your own APIs). Egress filtering converts a silent exfiltration into an alertable, blocked event.

  6. Deploy the detections above on developer endpoints and CI infrastructure specifically. This threat class targets build environments precisely because they are credential-rich and monitoring-poor. If your EDR coverage ends at servers, you are blind where these attacks land.

The Bottom Line

Install-script suppression was a correct answer to the last generation of npm attacks. The indexed-btree campaign is the adversary's answer to our answer — payload delivery moved from install time to runtime, from the locked-down build container to the credential-rich production process. Defenders who match that shift with behavioral detection on the node process, dependency vetting gates, egress controls, and CI secret minimization will close this gap. Defenders still relying on --ignore-scripts as their primary npm supply-chain control are operating with a documented blind spot that is being exploited today.

Related Resources

Security Arsenal Penetration Testing Services AlertMonitor Platform Book a SOC Assessment vulnerability-management Intel Hub

Is your security operations ready?

Get a free SOC assessment or see how AlertMonitor cuts through alert noise with automated triage.