Lorem, ipsum dolor sit amet consectetur adipisicing elit. Qui, itaque voluptate ipsa non enim amet ducimus voluptatibus deserunt nam esse!
Auditing the Shai-Hulud Worm: From npm Script to Stolen AWS, GitHub, and K8s Credentials

Auditing the Shai-Hulud Worm: From npm Script to Stolen AWS, GitHub, and K8s Credentials

pr0h0
npmgithubawskubernetes
AI Usage (91%)

What Shai-Hulud is, and why it matters to developers

Shai-Hulud is a developer-targeting worm that spreads through npm workflows and then hunts for high-value secrets on the host. The goal is not just one token. It is to turn a single compromised package or install step into access to GitHub, AWS, and Kubernetes environments that were never meant to be exposed to a random dependency.

That makes it different from a plain credential stealer. What makes it worth auditing is the way it targets the places developers already trust: package install hooks, local config files, CI runners, and shell environments. If your build process can run arbitrary package scripts, your dependency chain is also an execution chain.

The infection path through npm workflows

Why package scripts are such an effective entry point

npm scripts are powerful because they run with the same user privileges as the install process. That means a malicious preinstall, install, or postinstall script can read files, inspect environment variables, and spawn child processes without needing a browser exploit or a kernel bug.

A common audit mistake is to think “it’s just a package install.” It is not. It is code execution.

How install-time execution turns one compromised package into many hosts

The worm model works because developers reuse the same package versions across local machines, CI runners, and container builds. If one package gets poisoned, every fresh install becomes a possible infection point until the bad version is removed or pinned out.

This is the operational risk:

StageWhat happensWhy it matters
Local devnpm install runs scriptsSecrets in shell or dotfiles can be read
CIDependency install happens on shared runnersTokens and cloud creds are often present
Build imagePackage scripts run during image buildCompromise can be baked into artifacts

What the worm looks for on disk and in process environments

npm and GitHub credentials

The obvious targets are npm tokens and GitHub credentials because they show up often in developer environments and CI. npm auth tokens can let an attacker publish or pull packages. GitHub tokens can expose private repos, release pipelines, and workflow secrets.

I would expect a worm in this class to check environment variables first, then local config files such as .npmrc, GitHub CLI state, and cached credentials under the user profile. If those values are available, there is no need to brute force anything.

AWS keys, session tokens, and CLI profiles

AWS access keys are especially dangerous because one leaked key rarely stays limited to one service. A session token, even if short-lived, can still be enough to enumerate roles, read secrets, or access storage buckets before it expires.

A good local audit should check for:

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_SESSION_TOKEN
  • ~/.aws/credentials
  • ~/.aws/config

The detail that gets missed is that temporary credentials are not safe, just safer. If the worm runs during the token window, it still wins.

Kubernetes kubeconfig files and cluster access

Kubernetes access is attractive because kubeconfig often holds cluster endpoints, contexts, and bearer tokens. On a laptop or CI node, that can be enough to reach production-adjacent clusters.

If the infected process can read ~/.kube/config, the attacker may get more than cluster visibility. Depending on how the file is configured, it can expose a direct path to API access.

How exfiltration tends to happen in practice

Common outbound channels and why they are hard to spot

The stolen data usually leaves through normal outbound HTTP or HTTPS traffic. That is what makes this class of worm noisy in impact but quiet in network monitoring. It looks like a build tool calling home unless you are watching destination reputation, request timing, and unusual payload sizes.

A second issue is that exfiltration may be split into small chunks. That makes it easier to blend into ordinary telemetry or package metadata traffic.

The operational mistake that makes the leak persistent

The mistake I see most often is leaving long-lived credentials on developer machines and CI workers. If the same AWS key, GitHub token, or npm token sits in a shell profile for months, the worm only needs one successful run.

That turns a one-time infection into durable access.

Reproducing the risk safely in a local audit

Inspecting package scripts before install

Before running npm install on an untrusted package, inspect its manifest:

// quick-scan.js
const fs = require("fs");

const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8"));
console.log({
  scripts: pkg.scripts,
  dependencies: pkg.dependencies,
  devDependencies: pkg.devDependencies,
});

What you are looking for is not only obvious shell commands. Watch for scripts that launch Node with bundled code, call out to curl or bash, or invoke a postinstall helper.

Tracing filesystem access and spawned child processes

In a test environment, trace what the installer reads and launches. On Linux, strace or auditd can show reads of ~/.npmrc, ~/.aws/credentials, or ~/.kube/config. On macOS, use filesystem and process monitoring tools that can show child process creation.

The question is simple: did a package install touch more than its own dependencies and build artifacts?

Watching network egress from a test runner

Put the install in a sandbox and watch outbound connections. A safe test runner should be able to tell you:

  • which process opened the socket
  • which host it contacted
  • whether the payload was normal telemetry or credential-shaped data

You do not need to exfiltrate anything real to prove the risk. A dummy secret in a throwaway environment is enough to validate the path.

What actually reduces exposure

Locking down npm workflows and CI runners

Treat install scripts as code execution and reduce where they can run. Use pinned lockfiles, review dependency updates, and isolate CI runners so that package install jobs do not share broad secret access with deploy jobs.

Limiting token scope and shortening secret lifetime

Use the smallest scope that works and shorten lifetime wherever possible. If a token only needs read access for one job, do not hand it write permissions. If a session can expire in minutes instead of days, do that.

Hardening AWS and Kubernetes access paths

For AWS, prefer role-based access and session-scoped credentials over static keys. For Kubernetes, avoid leaving cluster-admin kubeconfigs on developer laptops and build hosts. The safest secret is the one the install step cannot read.

A practical checklist for developers and DevSecOps teams

  • review preinstall, install, and postinstall scripts before introducing a package
  • keep npm, GitHub, AWS, and kube credentials out of shell profiles when possible
  • separate build runners from deploy runners
  • rotate long-lived tokens and remove unused ones
  • monitor unexpected outbound requests during dependency installs
  • sandbox package installation for untrusted or newly published dependencies
  • verify that CI jobs only receive the secrets they actually need

Conclusion

Shai-Hulud matters because it uses ordinary developer workflows as the infection vector. That is the hard part to defend: the compromise does not need a fancy exploit if the build itself is allowed to execute attacker-controlled code.

If you want a real control, start with the install boundary. Treat every package script as potentially hostile, then make sure there is nothing valuable for it to steal.

Share this post

More posts

Comments