Supply Chain Crisis: Over 30 Red Hat npm Packages Hijacked to Spread the Self-Propagating ‘Miasma’ Worm

The CyberSec Guru

Red Hat npm Packages Compromised

If you like this post, then please share it:

Buy me A Coffee!

Support The CyberSec Guru’s Mission

🔐 Fuel the cybersecurity crusade by buying me a coffee! Why your support matters: Zero paywalls: Keep the main content 100% free for learners worldwide, Writeup Access: Get complete in-depth writeup with scripts access within 12 hours of machine drop.

“Your coffee keeps the servers running and the knowledge flowing in our fight against cybercrime.”☕ Support My Work

Buy Me a Coffee Button

If you ran npm install on anything under the @redhat-cloud-services scope today, stop what you’re doing.

On the morning of June 1, 2026, attackers quietly pushed backdoored versions of at least 32 packages across the official Red Hat npm scope which amounts to around 64 individual package versions in total all carrying a credential-stealing, self-replicating worm called Miasma. The malware executes the moment you run npm install. It doesn’t wait for your app to start. It doesn’t need to be imported. It just runs.

What makes this attack genuinely alarming isn’t just the breadth of the compromise. It’s how the attackers got there. They didn’t steal anyone’s login. They didn’t brute-force a token. They found a structural gap in how npm’s trusted publishing system handles GitHub Actions, one that let them mint a perfectly valid, provenance-attested package publish event using nothing but a temporary branch on Red Hat’s own repositories.

How the Attack Actually Worked

To understand why this is serious, you need to understand what npm’s Trusted Publishing feature is supposed to do.

Trusted Publishing also called OIDC publishing is a relatively recent addition to the npm registry. It’s meant to make CI/CD publishing more secure by tying a package’s publish authority to a specific GitHub repository and workflow file, removing the need to store long-lived npm tokens in your CI secrets. On paper, that’s a good idea. GitHub issues short-lived OIDC identity tokens to workflows, those tokens get exchanged for npm publish tokens, and you never have to manually manage credentials.

The problem, and it’s a fairly significant one in hindsight, is that the trust binding doesn’t include branches. The registry cares that the publish came from repository RedHatInsights/javascript-clients running workflow .github/workflows/publish.yml it does not care which branch that workflow ran on.

The attackers found this and used it methodically.

They targeted three repositories inside the RedHatInsights GitHub organization: javascript-clients, frontend-components, and platform-frontend-ai-toolkit. On each, they pushed short-lived branches with names following the pattern oidc-<hex> meaningless hex strings, presumably to avoid naming collisions or triggering obvious alerts. Because these were branches on the legitimate repositories (not forks), they triggered the repositories’ existing GitHub Actions workflows.

On each ephemeral branch, the attacker modified the trusted workflow YAML to add a malicious publishing job. That job requested an OIDC identity token from GitHub’s provider, which GitHub granted, because the workflow was legitimately running on a repo it recognized, then exchanged it for an npm publish token through npm’s trusted token exchange endpoint.

From there, the process was almost mechanical. The attacker’s script fetched the legitimate, current Red Hat package tarball, injected a malicious preinstall hook into the package.json, repackaged the tarball, and published it back to npm. Because the entire publish event originated from GitHub Actions on the official repository, npm generated a valid provenance attestation for the malicious package. Automated security scanners that check for provenance, the kind of thing a security-conscious team might have in their dependency review pipeline would have seen a green checkmark.

The branches were deleted after publishing, leaving minimal traces in the repository history.

The Attack Path
The Attack Path

The OIDC Trust Gap: A Deeper Look

This isn’t the first time branch-scope gaps in OIDC trust policies have been identified as a theoretical risk, but this appears to be the first time the attack has been executed at this scale against a major software vendor’s npm scope.

The fundamental issue is that GitHub Actions’ OIDC token contains claims that include the repository name, the workflow path, the triggering actor, and the ref (branch or tag). npm’s trusted publishing implementation validates the repository and workflow path but not the ref. That means any branch in a trusted repository can trigger a trusted publish event, as long as it can modify the workflow file.

For organizations where branch protection rules don’t restrict who can push directly to arbitrary branches, this is a viable attack path without needing any stolen credentials at all. The attacker needs only write access to create a branch, something that, depending on repository settings, may be achievable through a compromised contributor account with comparatively limited permissions.

Red Hat has not yet published a formal post-mortem at the time of writing, so it’s not fully clear whether the attackers obtained branch-push access through a compromised account or through some other means. What is clear is that once they had it, npm’s trust model did the rest of the work for them.

What Miasma Does Once It Runs

The malicious payload is delivered through a single added line in each compromised package’s package.json:

json

"scripts": {
"preinstall": "node index.js"
}

The preinstall hook is one of npm’s oldest lifecycle hooks. It runs before the package’s dependencies are resolved, before npm install completes, and critically before the developer gets any success notification. You don’t need to import or require anything. The moment npm starts installing the package, the payload is already executing.

One detail worth pausing on: several of the affected packages are type-only helper packages, such as @redhat-cloud-services/types. Type packages don’t do anything at runtime. They’re just TypeScript definitions. A type package with a preinstall script is an immediate red flag for anyone inspecting the package manifest manually but most developers don’t do that.

The Obfuscation Stack

The clean version of most of these packages’ index.js files is a simple export, typically under 8 KB. The compromised versions are between 4.2 MB and 4.3 MB a roughly 500x size increase that would be obvious to anyone diffing the tarball, but invisible to npm install itself.

That size comes from a multi-layer obfuscation chain:

Layer 1 – ROT-9 Encoded Loader: The outermost shell is an array of ROT-9-encoded character codes. ROT-9 is a trivial Caesar cipher (shift each character by 9), which defeats simple string searches or grep-based scanners looking for obvious keywords like exec or fetch. The loader reconstructs and evaluates a JavaScript string at runtime, specifically to escape static analysis tools that parse the file without executing it.

Layer 2 – AES-128-GCM Encrypted Blobs: Strip away Layer 1 and you find two AES-128-GCM encrypted binary blobs. The first is a lightweight bootstrapper; the second is the primary 634 KB payload written for the Bun runtime. Using an encrypted blob at this stage means that static malware scanners examining the package content, even ones that decode ROT-9, are looking at ciphertext until the key is derived at runtime.

Layer 3 – Bun Runtime Download: If the target machine doesn’t have the Bun JavaScript runtime installed, the bootstrapper fetches it from GitHub’s own official release mirror, specifically bun-v1.3.13, compiled for Linux, macOS, or Windows depending on the target. Using a download from github.com/oven-sh/bun/releases is intentional: many corporate network security tools and EDR solutions maintain allowlists for GitHub traffic, so this download often passes without inspection. The bootstrapper then executes the decrypted core payload as a temporary file under /tmp/p*.js and immediately deletes it, leaving no persistent payload artifact on disk.

The execution chain looks like this:

[npm install]
└─> preinstall hook: node index.js
└─> Layer 1: ROT-9 decode and eval
└─> Layer 2: AES-128-GCM decryption
└─> Layer 3: Fetch Bun runtime (if needed)
└─> Execute 634KB payload in memory
└─> Harvest, propagate, persist
└─> Delete payload file

What Gets Stolen

The core payload is a credential harvester that targets virtually every secret-bearing system a modern cloud developer is likely to have access to.

Cloud infrastructure credentials:

  • AWS: IMDSv2 metadata endpoint, ECS task role credentials, Secrets Manager configurations, SSM parameter values
  • GCP: Application Default Credentials (ADC), service account key files
  • Azure: Managed identity tokens, service principal credential files

CI/CD tokens: GITHUB_TOKEN, ACTIONS_RUNTIME_TOKEN, CircleCI tokens, GitLab runner credentials. If this runs on a CI server, it captures whatever the runner has access to, which is often quite a lot.

Orchestration and secrets managers: Kubernetes service account tokens, any kubeconfig profiles it can find, HashiCorp Vault tokens (VAULT_TOKEN, VAULT_AUTH_TOKEN).

Developer machine credentials: SSH private keys, GPG signing keys, Docker registry auth configs, .env files across the filesystem, and local password manager databases including Bitwarden and gopass vaults.

Bypassing CI Log Masking via /proc/mem

This is one of the more technically sophisticated elements of the payload, and it’s worth explaining clearly.

Modern CI platforms like GitHub Actions automatically mask sensitive environment variables in build logs. If AWS_SECRET_ACCESS_KEY is set in your workflow environment, the runner intercepts any log output containing that string and replaces it with ***. The intention is that even if someone later reads a build log, they can’t extract raw credentials from it.

Miasma doesn’t go through the log. On Linux environments, it reads /proc/mem directly – the raw process memory of the running environment. Environment variables, including those flagged for masking, are stored in memory as plain text. By reading memory directly, the malware extracts the raw, unmasked string values before any log scrubbing layer gets involved. The mask only applies to what gets written to the log output stream; it does nothing to the in-memory representation of the variable.

This is particularly concerning for build servers where sensitive tokens are loaded into the environment for legitimate purposes, a GitHub Actions runner that has a production deployment token in its environment, for example, hands that token to Miasma regardless of any masking configuration.

Worm Behavior: How It Spreads

Miasma isn’t a one-shot stealer. It actively attempts to spread.

npm Registry Propagation: After harvesting npm publish tokens from local credential stores or active sessions, the payload queries the registry to determine which scopes and packages the victim’s account can publish to. It then injects its own preinstall loader into those packages and publishes backdoored versions, completely bypassing npm’s 2FA prompts if the existing session token is still valid. 2FA protects the login step; it doesn’t re-authenticate active sessions.

Git Repository Injection: The payload crawls the filesystem for Git repositories. When it finds one, it inserts a modified CI configuration, often disguised as a codeql.yml or testing workflow which is designed to re-execute the loader during automated CI runs. This means even after you wipe your local node_modules, a compromised repository can reinfect a fresh clone during the next CI build.

IDE Persistence: This is where it gets particularly clever about surviving remediation.

For Claude Code users, the malware targets the .claude directory in the user’s home or project folder. Claude Code uses configuration files in this directory to manage workspace context and tool integrations. By injecting a hook here, Miasma ensures that any subsequent Claude CLI invocation re-executes the payload, even after the npm packages have been removed.

For VS Code users, it modifies .vscode/tasks.json or related workspace configuration files to register a background task that runs silently every time the workspace folder is opened.

Both of these persistence mechanisms are genuinely difficult to catch because they live in developer tooling directories that most malware scanners don’t inspect, and because their file paths are not obviously malicious.

The Miasma / Shai-Hulud Lineage

The Miasma campaign didn’t emerge from nowhere. It’s the latest iteration of a threat framework that has been evolving publicly for about six weeks.

April 22, 2026 – The first confirmed Shai-Hulud attack targeted @bitwarden/cli on npm, introducing the signature “Shai-Hulud: The Third Coming” marker – a reference to the sandworms in Frank Herbert’s Dune series. At this stage, the attack was notable but limited in scope.

April 29–30, 2026 – The same framework, or a close derivative, was used to compromise SAP npm packages and PyTorch Lightning on PyPI, the Python package index. Expanding to PyPI indicated the attackers weren’t constrained to the JavaScript ecosystem.

May 12, 2026 – A substantially larger coordinated wave hit more than 160 npm packages, including load-bearing utilities used by Mistral and TanStack. Shortly after this wave, a group called TeamPCP published the full Shai-Hulud source code to GitHub and advertised it on BreachForums, making the underlying framework available to other threat actors.

May 19, 2026 – Microsoft’s DurableTask npm package was compromised, in this case through a hijacked GitHub account rather than an OIDC exploit.

June 1, 2026 – The Miasma campaign. The Greek mythology rebranding (“Miasma: The Spreading Blight”) and the shift from stolen GitHub accounts to a structural OIDC pipeline exploit indicate this is likely a separate actor building on the now-public Shai-Hulud framework rather than the original TeamPCP group. The technical refinements – altered exfiltration channels, updated static analysis evasions are consistent with someone who studied the original code and improved it.

The timeline from “framework published publicly” (May 12) to “new actor executes refined campaign” (June 1) is about three weeks. That’s a short turnaround, and it’s a reasonable preview of what’s likely coming from other actors who downloaded the same code.

Compromised Packages

If you installed or updated any of the following packages since June 1, 2026, treat your environment as compromised.

PackageCompromised Versions
@redhat-cloud-services/chrome2.3.1, 2.3.2
@redhat-cloud-services/compliance-client4.0.3, 4.0.4
@redhat-cloud-services/config-manager-client5.0.4, 5.0.5
@redhat-cloud-services/entitlements-client4.0.11, 4.0.12
@redhat-cloud-services/eslint-config-redhat-cloud-services3.2.1, 3.2.2
@redhat-cloud-services/frontend-components7.7.2, 7.7.3
@redhat-cloud-services/frontend-components-advisor-components3.8.2
@redhat-cloud-services/frontend-components-config6.11.3, 6.11.4
@redhat-cloud-services/frontend-components-config-utilities4.11.2, 4.11.3
@redhat-cloud-services/frontend-components-notifications6.9.2, 6.9.3
@redhat-cloud-services/frontend-components-remediations4.9.2, 4.9.3
@redhat-cloud-services/frontend-components-testing1.2.1, 1.2.2
@redhat-cloud-services/frontend-components-translations4.4.1, 4.4.2
@redhat-cloud-services/frontend-components-utilities7.4.1, 7.4.2
@redhat-cloud-services/hcc-feo-mcp0.3.1, 0.3.2
@redhat-cloud-services/hcc-kessel-mcp0.3.1, 0.3.2
@redhat-cloud-services/hcc-pf-mcp0.6.1, 0.6.2
@redhat-cloud-services/host-inventory-client5.0.3, 5.0.4
@redhat-cloud-services/insights-client4.0.4, 4.0.5
@redhat-cloud-services/integrations-client6.0.4, 6.0.5
@redhat-cloud-services/javascript-clients-shared2.0.8, 2.0.9
@redhat-cloud-services/notifications-client6.1.4, 6.1.5
@redhat-cloud-services/patch-client4.0.4, 4.0.5
@redhat-cloud-services/quickstarts-client4.0.11, 4.0.12
@redhat-cloud-services/rbac-client9.0.3, 9.0.4
@redhat-cloud-services/remediations-client4.0.4, 4.0.5
@redhat-cloud-services/rule-components4.7.2, 4.7.3
@redhat-cloud-services/sources-client3.0.10, 3.0.11
@redhat-cloud-services/topological-inventory-client3.0.10, 3.0.11
@redhat-cloud-services/tsc-transform-imports1.2.2
@redhat-cloud-services/types3.6.1, 3.6.2, 3.6.4
@redhat-cloud-services/vulnerabilities-client2.1.8, 2.1.9

What to Do Right Now

Step 1: Contain

Kill any active CI/CD build runners operating on the affected repositories immediately. A runner that’s currently mid-build on a compromised package is actively exfiltrating credentials.

Delete your node_modules directories and lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml) on every machine that may have installed these packages. Don’t just reinstall instead delete first.

Then manually inspect your .claude/ and .vscode/ directories for anything you didn’t put there. Look specifically for scripts or task definitions that reference temp paths, encode commands in base64, or make network calls. Delete anything suspicious.

Step 2: Rotate Everything

Because Miasma reads raw process memory, you cannot assume that a credential which was merely present on a compromised machine is safe, even if you never logged it anywhere. Rotate:

  • AWS IAM credentials and any active sessions
  • GCP service account keys
  • Azure service principal secrets and managed identity tokens
  • Kubernetes service account tokens; rebuild your .kube/config
  • npm and PyPI publishing tokens
  • SSH keys and GPG signatures
  • Any secrets stored in .env files on affected machines

Step 3: Check Your CI Pipelines

Review your GitHub Actions workflow files for unexpected jobs or steps added to recent commits. Check branch protection settings on any repositories configured for npm trusted publishing and specifically, ensure that direct branch pushes require review, and ideally add branch name constraints to your OIDC trust configuration if your workflow platform supports it.

Frequently Asked Questions

Does npm 2FA protect against Miasma hijacking my packages?

No. Miasma collects active, authenticated session tokens from your local environment – the tokens that were already issued after your 2FA login. It uses those active sessions to publish, bypassing the MFA prompt entirely because the session is already authenticated.

Would my EDR have caught this?

Probably not, and that’s worth understanding. The malware downloads the Bun JavaScript runtime directly from GitHub’s official release URLs, a legitimate developer tool from a trusted domain. Most signature-based EDR tools don’t flag Bun because it’s a real, widely used runtime. The actual malicious payload runs from a temp file that’s deleted almost immediately after execution.

How did attackers get into Red Hat’s repositories?

They may not have needed deep access at all. The OIDC exploit requires the ability to push a branch to the repository and modify a workflow file – branch-level write access, not admin access. Whether this was obtained through a compromised contributor account with limited permissions or something else isn’t yet confirmed.

Is Miasma the same thing as Shai-Hulud?

It’s a derivative built from the same public codebase. The original Shai-Hulud framework was published by TeamPCP on May 12; Miasma replaces the Dune mythology references with Greek mythology ones, uses different exfiltration infrastructure, and adds several evasion improvements specifically aimed at detectors that were written in response to the May 12 wave. Different branding, meaningfully different code, probably different operators.

The Bigger Picture

Something worth saying plainly: the branch-scope gap in npm’s OIDC trusted publishing is a real design issue, not just a misconfiguration on Red Hat’s part. If your OIDC trust policy binds to a repository and a workflow filename but not to a branch, then anyone who can push to any branch on that repository can trigger a trusted publish event. That’s a lot of organizations that may not have thought through that implication.

The Shai-Hulud codebase going fully public in mid-May lowered the barrier significantly. You no longer need to write sophisticated supply-chain malware from scratch, you can adapt a working framework in a matter of weeks, as this campaign demonstrates. Expect more of this.

The remediation steps above are correct but they’re also reactive. The structural question, how package registries verify publish authority in a world where CI/CD pipelines are the de facto publishing mechanism needs an answer that doesn’t leave branch pushes as an unguarded path.

Buy me A Coffee!

Support The CyberSec Guru’s Mission

🔐 Fuel the cybersecurity crusade by buying me a coffee! Your contribution powers free tutorials, hands-on labs, and security resources.

Why your support matters:
  • Writeup Access: Get complete writeup access within 12 hours
  • Zero paywalls: Keep the main content 100% free for learners worldwide

Perks for one-time supporters:
☕️ $5: Shoutout in Buy Me a Coffee
🛡️ $8: Fast-track Access to Live Webinars
💻 $10: Vote on future tutorial topics + exclusive AMA access

“Your coffee keeps the servers running and the knowledge flowing in our fight against cybercrime.”☕ Support My Work

Buy Me a Coffee Button

If you like this post, then please share it:

News

Discover more from The CyberSec Guru

Subscribe to get the latest posts sent to your email!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Discover more from The CyberSec Guru

Subscribe now to keep reading and get access to the full archive.

Continue reading