Skip to main content
Worm Turns CI Pipelines Into Attack InfrastructureIncident
4 min readFor Security Engineers

Worm Turns CI Pipelines Into Attack Infrastructure

What Happened

Between late 2024 and early 2025, a worm named SHA1-Hulud infiltrated the npm ecosystem through trojanized packages with hidden preinstall scripts. Snyk identified over 600 compromised npm packages, including those from Zapier, PostHog, and Postman. The attack transformed infected development machines into rogue GitHub Actions runners, harvested cloud credentials, and sometimes attempted to wipe user data.

This was not a single compromise — it was self-propagating malware that used your CI/CD infrastructure as its distribution network.

Timeline

Initial Infection Vector: Developers unknowingly installed trojanized npm packages during routine dependency updates. The malicious code executed via preinstall scripts before most security scanning could intervene.

Propagation Phase: Once on a developer machine, the worm:

  • Harvested AWS, GCP, and Azure credentials from environment variables and configuration files
  • Registered compromised machines as GitHub Actions self-hosted runners
  • Injected malicious code into subsequent package builds

Lateral Movement: The rogue CI runners executed on legitimate workflow triggers, spreading the infection to:

  • Other repositories in the same organization
  • Downstream packages that depended on compromised libraries
  • Production environments with access to the CI system

Advanced Persistence: When credential harvesting failed, the worm attempted container breakouts and privilege escalation to maintain access.

Which Controls Failed or Were Missing

Dependency Verification: The infected packages passed initial installation because npm's package verification only validates signatures, not behavior. Your package manager installed and executed code before any runtime analysis occurred.

Script Execution Policy: The preinstall scripts ran with the same privileges as the installing user. There was no sandboxing, permission model, or execution review. If you can npm install, the package can execute arbitrary code on your system.

Secrets Management: Credentials stored in environment variables (AWS_ACCESS_KEY_ID, GOOGLE_APPLICATION_CREDENTIALS) and configuration files (.aws/credentials, .config/gcloud) were readable by any process. The worm didn't need to exploit anything — it just read files your shell could read.

CI Runner Authentication: GitHub Actions self-hosted runners authenticate with a registration token, but there's no mechanism to verify that a runner claiming to be "prod-builder-03" is actually the machine you provisioned. The worm registered fake runners, and GitHub accepted them.

Network Segmentation: CI systems with production credentials had network access to development machines. Once the worm compromised a developer laptop, it could reach systems that should have been isolated.

What the Standards Require

PCI DSS v4.0.1 Requirement 6.3.2 mandates that you review custom code before release to production to identify potential coding vulnerabilities. Preinstall scripts are custom code executing in your build environment. Your review process should include:

  • Static analysis of package scripts before installation
  • Sandboxed execution environments for dependency installation
  • Behavioral monitoring during the build process

Requirement 8.2.2 requires multi-factor authentication for all access to system components. Your CI runners access production systems — they need MFA-protected service accounts, not long-lived credentials in environment variables.

NIST 800-53 Rev 5 Control SA-10 (Developer Configuration Management) requires you to control changes to software during development. When a package's preinstall script can modify your build environment without review, you've lost configuration management. You need:

  • Immutable build environments that reset between jobs
  • Explicit approval for any script execution during dependency installation
  • Version control for all code that runs in your CI pipeline, including third-party scripts

Control SC-7 (Boundary Protection) requires you to monitor and control communications at external boundaries and key internal boundaries. Your CI system is a key internal boundary. The SHA1-Hulud worm crossed this boundary because:

  • Developer machines could initiate connections to CI systems
  • CI runners could register without cryptographic verification
  • No behavioral analysis flagged unusual runner registration patterns

ISO 27001 Control 8.31 (Separation of Development, Test and Production Environments) requires logical or physical separation. If your CI system can write to production and your developer laptop can write to your CI system, you don't have separation — you have a continuous privilege escalation path.

Lessons and Action Items for Your Team

Audit Your npm Scripts Today: Run npm config set ignore-scripts true globally, then audit each package's scripts before allowing execution. This may break some legitimate packages, but it's necessary to know what's executing.

Implement Script Sandboxing: Use container-based builds with:

  • Read-only filesystem mounts for source code
  • Explicit network egress rules (block by default)
  • Separate containers for dependency installation vs. application build
  • No persistent state between builds

Eliminate Ambient Credentials: Replace environment variable credentials with:

  • OIDC federation for cloud provider access (GitHub Actions supports this natively)
  • Short-lived tokens from a secrets manager, fetched at runtime
  • Workload identity that binds to specific jobs, not runners

Verify CI Runner Identity: For self-hosted runners:

  • Use runner groups with repository-level restrictions
  • Implement runner registration approval workflows
  • Monitor runner registration events in your SIEM
  • Rotate runner tokens on a schedule (treat them like passwords)
  • Consider GitHub-hosted runners for non-sensitive workloads

Network Isolation for CI: Your CI system should:

  • Run in a separate network segment from developer machines
  • Use a bastion host or VPN for any administrative access
  • Implement egress filtering (CI jobs shouldn't access arbitrary internet hosts)
  • Log all network connections for behavioral analysis

Dependency Pinning and Verification: Lock your dependencies to specific versions with cryptographic hashes:

"dependencies": {
  "package-name": "1.2.3",
  "integrity": "sha512-..."
}

Use npm ci instead of npm install in CI to enforce exact versions.

Behavioral Monitoring: Deploy endpoint detection on build systems that flags:

  • Unexpected outbound connections from build processes
  • File access patterns inconsistent with normal builds
  • Process execution chains that include shell spawning from node

The SHA1-Hulud worm succeeded because it exploited the trust boundary between package installation and execution. Your package manager assumed packages were safe. Your CI system assumed runners were legitimate. Your secrets management assumed the environment was controlled.

Each assumption created a control gap. Close them.

Topics:Incident

You Might Also Like