Executive Summary

The security of the npm ecosystem reached a critical inflection point in September 2025. The Shai-Hulud worm, a self-replicating malware that automated the compromise and redistribution of malicious packages, marked the end of the “nuisance” era of npm attacks and the beginning of a high-consequence threat landscape.

Since that watershed moment, Unit 42 has tracked an aggressive acceleration in the frequency and technical depth of supply chain compromises. Attacks have evolved from a series of isolated typosquatting incidents into systematic campaigns by various threat actors to weaponize the trust that powers modern software development.

The New Baseline for npm Threats

The Shai-Hulud incident proved that the npm registry could be used as a force multiplier for malware distribution. In the months following, we have observed three core shifts in adversary TTPs:

  • Wormable propagation: Malicious payloads now prioritize the theft of npm tokens and GitHub Personal Access Tokens (PATs) to automatically infect and republish legitimate packages, as seen in the March 2026 Axios compromise.
  • Infrastructure-level persistence: Attackers are no longer just stealing data; they are embedding themselves into continuous integration/continuous delivery (CI/CD) pipelines to attain long-term, undetectable access to enterprise environments.
  • Multi-stage payloads: Following the September 2025 template, current attacks often deploy dormant “sleeper” dependencies that only activate under specific environmental conditions to evade automated scanners.

npm Attacks Seen As a Whole

npm compromises have common themes. In the post-Shai-Hulud era, we believe it is helpful to consider the attack surface as a whole.

This article will combine:

  1. Details of major incidents: Real-time analysis of significant package compromises (e.g., Shai-Hulud 2.0, Axios, Chalk/Debug)
  2. Cross-campaign correlation: Identifying common infrastructure or code snippets that link disparate attacks to the same threat actors
  3. Remediation playbooks: Actionable guidance for rotating credentials and purging malicious dependencies from local and cloud-based caches

Shai-Hulud: A New Wave

A malicious npm package published as @bitwarden/cli version 2026.4.0 was identified as part of a broader supply-chain campaign attributed to TeamPCP. The package impersonates the legitimate Bitwarden CLI password manager. Upon installation, it executes a multi-stage payload that steals credentials from cloud providers, CI/CD systems and developer workstations. It then self-propagates by backdooring every npm package the victim can publish. It has been noted that inside public GitHub repositories that were published contained the string “Shai-Hulud: The Third Coming.”

Attackers deployed the same payload across multiple Checkmarx distribution channels, indicating a coordinated campaign to weaponize compromised developer tooling credentials to maximize the area of impact:

  • Docker Hub images
  • GitHub Actions
  • VS Code extensions

Palo Alto Networks customers are better protected from the threats described in this article through the following products and services:

The Unit 42 Incident Response team can also be engaged to help with a compromise or to provide a proactive assessment to lower your risk.

Related Unit 42 Topics Supply Chain, Credential Harvesting, Obfuscation, Backdoor

April 2026 - Shai Hulud: A New Wave

Broader Campaign Context

According to Checkmarx's official security update, this npm package is one component of a broader supply-chain campaign that simultaneously compromised multiple Checkmarx distribution channels:

  • Docker Hub: Poisoned checkmarx/kics images (v2.1.20, v2.1.21, latest, alpine, debian)
  • GitHub Actions: Malicious checkmarx/ast-github-action v2.3.35
  • VS Code extensions: Backdoored checkmarx/ast-results (v2.63, v2.66) and checkmarx/cx-dev-assist (v1.17, v1.19)
  • npm: The @bitwarden/cli package analyzed in this report

Per Checkmarx's disclosure, all artifacts share the same C2 infrastructure (audit.checkmarx[.]cx), the same obfuscation techniques and the same credential harvesting and propagation logic. The VS Code extension variant delivered its payload (mcpAddon.js) from a backdated orphan commit in Checkmarx's own GitHub repository, making the download URL appear trustworthy.

TeamPCP (@pcpcats) publicly took credit for the compromise. Per Socket's analysis, the group had previously targeted Checkmarx infrastructure in March 2026, along with Trivy and LiteLLM, suggesting an ongoing campaign against security tooling vendors.

Attack Overview

Table 1 shows the attributes of the attack.

Attribute Detail
Package @bitwarden/cli@2026.4.0
Trigger preinstall lifecycle script
Runtime Bun v1.3.13 (downloaded during install)
C2 server audit.checkmarx[.]cx:443 (94.154.172[.]43)
C2 path /v1/telemetry
Fallback C2 Dynamic, fetched via GitHub Search API dead drop
Exfiltration HTTPS POST (encrypted) + GitHub public repos
Attribution TeamPCP (@pcpcats)

Table 1. Attributes of the attack.

Stage 1: Bootstrap - bw_setup.js

The package.json provides two execution paths for the malicious script, as shown in Figure 1.

Screenshot of a code snippet in JSON format. It includes a "scripts" section with a "preinstall" key running node and a "bin" section with "bw".
Figure 1. Execution paths for the malicious script in the package.json file.

The preinstall hook runs automatically during npm install. The bin field registers bw_setup.js as the bw command, symlinking it into the user's PATH.

Since the legitimate Bitwarden CLI also uses bw as its binary name, this serves as a secondary trigger. Even if preinstall is blocked (e.g., via --ignore-scripts), the malware executes the next time the user or any script invokes bw. The shebang line #!/usr/bin/env node at the top of bw_setup.js ensures it runs as a Node.js script when called directly.

The bootstrap script performs three actions:

  1. Platform detection: Identifies the OS and architecture (Linux, macOS, Windows; x64 or arm64), including musl versus glibc detection on Linux.
  2. Bun runtime download: Downloads the Bun JavaScript runtime (v1.3.13) from the official github[.]com/oven-sh/bun releases. This is needed because the main payload uses Bun-specific APIs (shell execution, file I/O, gzip) not available in Node.js.
  3. Payload execution: Runs bw1.js using the freshly downloaded Bun binary.

A custom ZIP extraction implementation is included to avoid any dependencies, making the bootstrap entirely self-contained.

Stage 2: The Payload - bw1.js

The payload is an approximately 10 MB single-line JavaScript file containing approximately 285,000 lines when formatted. It bundles legitimate software developer kits (SDKs) (e.g., AWS SDK, Google Cloud client libraries, Azure Identity, Octokit, jsonwebtoken, tar) alongside the malicious orchestration code.

Obfuscation Techniques

The code employs multiple layers of obfuscation:

  • String table rotation: A function _0x214e resolves hex indices to strings from a large rotated array (_0x1ee1), breaking simple static string analysis.
  • Seeded ASCII shuffle cipher: Sensitive strings (domains, file paths, shell commands) are encoded as arrays of numeric indices into a Fisher-Yates-shuffled ASCII table. The shuffle uses a linear congruential PRNG seeded with 0x3039 (12345), as shown below in Figure 2.
Screenshot of a snippet showing a class named PRNG with a constructor and next methods. The constructor sets a seed value, while the next method recalculates the seed using a specific formula and returns it after division by a large number.
Figure 2. Seeded ASCII shuffle cipher.

The 128-character ASCII set is shuffled deterministically, producing a substitution table where an index with the hex value 0x42 maps to the ASCII character a, 0x6e to u. For example, the C2 domain is stored as [0x42, 0x6e, 0x36, 0x4b, 0x2b, 0x5c, 0xd, 0x57, 0x0, 0xd, 0x7, 0x26, 0x42, 0x3, 0x2a, 0x5c, 0xd, 0x2a], which decodes to an ASCII string for the domain audit.checkmarx[.]cx.

  • Gzip and Base64 embedded payloads: Several blobs are stored as gzip-compressed Base64 strings, including an RSA public key, a GitHub Actions workflow YAML, the worm's setup.mjs loader and a manifesto string
  • Mangled identifiers: All variable and function names are replaced with hex patterns such as _0x3865d8

Credential Harvesting

The payload deploys multiple provider classes, each targeting a different credential source. Every provider scans its results with regex patterns to extract npm and GitHub tokens, as shown in Figure 3.

Screenshot of a code snippet showing two regular expressions for detecting tokens: one for 'ghtoken' and another for 'npmtoken', both following a specific pattern.
Figure 3. Regex patterns to extract npm and GitHub tokens.
File System Provider (Cn)

Reads sensitive files from the developer's workstation, with per-OS path lists decoded via the scrambler as shown below in Table 2.

Platform Targeted Files
Linux ~/.ssh/id_*, ~/.ssh/keys, .git/config, ~/.npmrc, .npmrc, .env, ~/.claude/mcp.json, ~/.claude.json, ~/.kiro/settings/mcp.json
macOS ~/.aws/credentials, .git/config, ~/.npmrc, .npmrc, .env, ~/.claude.json, .claude.json, ~/.kiro/settings/mcp.json, .kiro/settings/mcp.json
Windows Credential store paths, config.ini

Table 2. OS path lists from the malware.

Files larger than approximately 5 MB are skipped. All others are read in full and included in the exfiltration payload.

Shell Provider (un)

Runs gh auth token via execSync to capture the GitHub CLI's active token, and then harvests the full process.env environment block. Both are returned for token-regex scanning and exfiltration. Unlike the cloud providers below, the Shell Provider does not use any SDK — it relies solely on the single shell command and environment variables.

GitHub Actions Provider (Co)

Detects CI/CD environments via process.env.GITHUB_ACTIONS === "true" and extracts all configured secrets by parsing {"value":"...","isSecret":true} patterns from the Actions runner context.

AWS SSM Provider (ku)

Uses the bundled AWS SDK (SSMClient, DescribeParameters, GetParameter) to enumerate and read parameters from AWS Systems Manager Parameter Store.

Azure Key Vault Provider (Rn)

Uses Azure Identity and Key Vault SDK to call getSecretsFromVault, listing and reading secrets from all accessible vaults.

Google Cloud Secret Manager Provider (uo)

Uses the Google Cloud Secret Manager client to listSecrets and accessSecretVersion, reading all accessible secrets across projects.

Exfiltration

All harvested credentials are processed through a two-tier exfiltration system.

Primary: HTTPS to C2 (Cy Class)
  1. Results are serialized to JSON and compressed with gzip
  2. A random 32-byte AES-256-GCM symmetric key is generated
  3. The data is encrypted with AES-256-GCM
  4. The symmetric key is RSA-encrypted with a hard-coded 4096-bit public key (Fr)
  5. The encrypted envelope is sent via POST hxxps[:]//audit.checkmarx[.]cx:443/v1/telemetry.
Secondary: GitHub Public Repositories (ky Class)

Using stolen GitHub tokens, the malware:

  1. Creates a new public repository under the victim's account with the description "Checkmarx Configuration Storage" and a Dune-themed name (e.g., gesserit-melange-813)
  2. Commits encrypted result files to a results/ directory
  3. Embeds stolen tokens in commit messages using the format LongLiveTheResistanceAgainstMachines:<base64_token>

This dual exfiltration ensures data reaches the attacker even if the C2 domain is blocked.

GitHub Actions Workflow Injection (u_ Class)

When the malware obtains a GitHub token with repo and workflow scopes, class u_ validates the scopes and then:

  • Enumerates repositories the victim can push to, prioritizing recently active ones
  • Checks whether each repo has configured GitHub Actions secrets (skips repos without them)
  • Creates a new branch, commits .github/workflows/format-check.yml — a malicious workflow that dumps all secrets, as shown below in Figure 4
Screenshot of a a YAML configuration script for a GitHub Actions workflow. The workflow name is "Formatter" it uses the "actions/checkout" and "upload-artifact" actions. The workflow is configured to output results to a specific file.
Figure 4. Content of the format-check.yml file.
  • Downloads the resulting artifact containing all serialized secrets
  • Deletes the branch and workflow run to cover its tracks

npm Worm Propagation

This is the self-replicating component. When the malware finds an npm token (npm_...):

Token Validation

Calls the npm tokens API with Authorization: Bearer <token>, verifies the token has publish (write) permission on the package scope and that it bypasses two-factor authentication (2FA).

Package Enumeration

Using the validated token:

  • Fetches the npm username
  • Iterates through the token's allowed resources (orgs, scopes, specific packages)
  • Queries /-/org/<user>/package for writable packages
  • Falls back to /-/v1/search?text=maintainer:<user> for public packages
Tarball Download and Backdooring

For each target package:

  • Downloads the latest tarball from the npm registry
  • Extracts it to a temporary directory
  • Copies the Bun binary into the package
  • Rewrites package.json:
    • Clears all scripts
    • Sets "preinstall": "node setup.mjs" (decoded from the scrambler)
    • Bumps the patch version (e.g., 1.2.3 → 1.2.4)
  • Writes setup.mjs with the full worm payload (K$, a gzip+Base64 encoded copy of the malware)
  • Re-packs the tarball
Publishing

Writes a .npmrc with the stolen token's auth line (//registry.npmjs[.]org/:_authToken=<token>) and runs bun publish <tarball>.

Every newly infected package will trigger the same preinstall chain on any developer who installs it, creating an exponential propagation vector.

C2 Resilience: The GitHub Dead Drop

The malware implements a fall back mechanism for C2 resilience using GitHub's public search API as a covert command channel.

If the primary C2 server (audit.checkmarx[.]cx) is unreachable, the malware:

  1. Searches GitHub commits for the keyword beautifulcastle via the public Search API
  2. Finds a matching commit in [helloworm00/hello-world](hxxps[:]//github[.]com/helloworm00/hello-world) (commit bc544f4, dated April 21 - one day before the main attack)
  3. Parses the commit message body with the regex /beautifulcastle ([A-Za-z0-9+/=]{1,30})\.([A-Za-z0-9+/=]{1,700})/
  4. Decodes the first capture group from Base64 to get a domain
  5. Verifies the second capture group as a digital signature (512-byte RSA-4096) against the hard-coded public key Er, ensuring only the attacker can direct the malware to a new C2 server

At the time of analysis, the dead drop contained a placeholder value (hxxps[:]//example[.]com), indicating it was pre-staged for future use. The attacker can rotate C2 infrastructure at any time by creating a new signed commit on any public GitHub repository.

A second fall back (Hr function) searches for commits matching LongLiveTheResistanceAgainstMachines:<base64> to recover previously stolen GitHub tokens from exfiltration repos, bootstrapping new GitHub-based exfiltration channels without any attacker-controlled infrastructure.

Anti-Detection Measures

  • Russian locale killswitch: The mz0 function checks Intl.DateTimeFormat().resolvedOptions().locale, LC_ALL, LC_MESSAGES, LANGUAGE and LANG for Russian (ru). If any match, the malware calls process.exit(0) immediately without executing
  • Daemonization: The Ml0 function re-spawns the process detached with __DAEMONIZED=1 in the environment, allowing the parent to exit cleanly while the payload runs in the background
  • PID-based lock file: $l0 writes process.pid to a temp file and checks whether a previous instance is still alive via process.kill(pid, 0), preventing multiple instances from running simultaneously
  • Signal handlers: Catch SIGINT/SIGTERM with no-op callbacks (() => {}) to prevent interruption
  • Temp directory cleanup: After tarball manipulation removes forensic artifacts
  • All sensitive strings: Encoded via the scrambler or gzip+Base64
  • Silent error handling: Failures are caught and suppressed
  • Innocuous naming: The C2 path v1/telemetry mimics legitimate analytics endpoints

Interim Guidance

  1. Block the C2 domains and IPs listed above at the network perimeter.
  2. Rotate all credentials that may have been exposed: npm tokens, GitHub PATs, AWS/Azure/Google Cloud keys, SSH keys and CI/CD secrets.
  3. Audit npm packages you maintain for unauthorized version bumps or new preinstall hooks.
  4. Review GitHub for unauthorized repository creation, unexpected workflow files and artifact downloads.
  5. Search for the format-results artifact in GitHub Actions logs across your organization.
  6. Hunt for unexpected Bun process execution and outbound connections to the IoC infrastructure.
  7. Pin dependencies to known-good versions using lockfiles and integrity hashes.

Conclusion

Unit 42 has witnessed a shift since the September 2025 Shai-Hulud incident, proving that it wasn’t a temporary spike but the new baseline for software supply chain risk. In an ecosystem where code is shared at the speed of thought, a single compromised dependency can trigger a global cascade.

Ultimately, npm compromises share commonalities and organizations can navigate this volatility by keeping particular best practices in mind. As we continue to monitor, analyze and update our findings related to npm packages, we encourage you to move beyond static defenses and embrace a culture of continuous verification. The supply chain may be the new primary target, but with collective intelligence and relentless visibility, it doesn’t have to be the primary vulnerability.

Palo Alto Networks customers are better protected by our products, as listed below. We will update this threat brief as more relevant information becomes available.

Mitigations for Compromised npm Packages

Enforce Cooldown Periods

Implement a policy (via a private registry or proxy like Artifactory) that blocks any package version published within the last 24 to 72 hours. Most malicious packages are identified and removed from the public registry within this window.

Disable Lifecycle Scripts

Many compromises rely on preinstall or postinstall hooks to exfiltrate secrets. Use the following in your .npmrc: ignore-scripts=true.

Version Pinning and npm ci

Use package-lock.json and ensure your CI/CD pipelines use npm ci instead of npm install. This prevents the "hidden" update of dependencies during a build.

Private Registry Proxying

Never allow developer machines or CI runners to talk directly to registry.npmjs[.]org. Route all traffic through a private registry.

Namespace Shadowing (Prevention of Dependency Confusion)

Attackers often publish packages with the same name as your internal libraries to the public registry. Always use scoped packages (e.g., @myorg/internal-lib) and configure your private registry to only resolve that scope internally.

Provenance Verification

Verify the OpenID Connect Attestation. Many major packages provide "provenance," proving the code was built on a specific GitHub/GitLab runner. Use tools like slsa-verifier to check these during the build.

Egress Filtering in CI/CD

Most npm-based malware attempts to send ~/.npmrc tokens or ~/.ssh keys to a C2 server. Apply strict egress network policies to your CI runners. Only allow connections to your private registry and known deployment targets.

Software Bill of Materials (SBOM)

Automatically generate an SBOM for every production release. This allows your security team to perform instant impact analysis when a new zero-day is announced.

Palo Alto Networks Product Protections Related to Compromised npm Packages

Palo Alto Networks customers can leverage a variety of product protections and updates to identify and defend against this threat.

If you think you might have been compromised or have an urgent matter, get in touch with the Unit 42 Incident Response team or call:

  • North America: Toll Free: +1 (866) 486-4842 (866.4.UNIT42)
  • UK: +44.20.3743.3660
  • Europe and Middle East: +31.20.299.3130
  • Asia: +65.6983.8730
  • Japan: +81.50.1790.0200
  • Australia: +61.2.4062.7950
  • India: 000 800 050 45107
  • South Korea: +82.080.467.8774

Advanced WildFire

The Advanced WildFire machine-learning models and analysis techniques have been reviewed and updated in light of indicators associated with npm compromises, including the malicious Bitwarden package.

Cloud-Delivered Security Services for the Next-Generation Firewall

Advanced URL Filtering and Advanced DNS Security identify known domains and IP addresses associated with this activity as malicious.

Cortex Cloud

Cortex Cloud’s Application Security Module (ASPM) supports the scanning of npm packages installed on cloud resources as well as monitoring audit logs from third party SaaS vendors, including GitHub as discussed within this article. Cortex Cloud prioritizes alerts, issues, policies and assets based on ingested applications as well as their usage. This allows security teams to maintain security awareness across their on-premises and cloud environment by identifying and remediating impacted cloud resources and actively responding to associated runtime operations from the threats discussed within this article through Cortex Cloud’s XDR Agent and serverless operations.

Indicators of Compromise

Network Indicators

Table 3 lists the network indicators from this activity.

Indicator Type
audit.checkmarx[.]cx C2 domain
94.154.172[.]43 C2 IP address
checkmarx[.]cx Attacker-controlled domain
91.195.240[.]123 Attacker IP address

Table 3. Network indicators.

GitHub Indicators

Table 4 lists the GitHub indicators from this activity.

Indicator Type
helloworm00/hello-world Dead drop repository
bc544f455d7c06c8a1f3446160a6d9a4a8236b11 Dead drop commit SHA1 hash
helloworm00@proton[.]me Attacker email address
Commit messages matching LongLiveTheResistanceAgainstMachines:* Exfiltration staging
Public repositories named <dune-word>-<dune-word>-<3digits> with description "Checkmarx Configuration Storage" Exfiltration repositories

Table 4. GitHub indicators.

Files and Process Indicators

Table 5 lists the file and process indicators from this activity.

Indicator Type SHA256 hash
bw_setup.js Bootstrap script f35475829991b303c5efc2ee0f343dd38f8614e8b5e69db683923135f85cf60d
bw1.js Obfuscated payload 18f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb
package.json Malicious manifest 167ce57ef59a32a6a0ef4137785828077879092d7f83ddbc1755d6e69116e0ad
setup.mjs in infected packages Worm payload
Unexpected bun process execution Runtime indicator
.github/workflows/format-check.yml on transient branches Workflow injection
format-results workflow artifact Secret exfiltration

Table 5. File and process indicators.

npm Indicators

Table 6 lists the npm indicators from this activity.

Indicator Type
@bitwarden/cli@2026.4.0 Malicious package
New preinstall: "node setup.mjs" in package.json Injected hook

Table 6. npm indicators.

Additional References

Enlarged Image