What Happened
Miasma is a supply chain worm discovered in late June 2026 that spread through the npm registry by compromising the OIDC-based publish tokens used in Red Hat’s open-source CI/CD pipelines. The attacker group tracked as TeamPCP exploited a misconfiguration in GitHub Actions workflows across 32 Red Hat-maintained npm packages to inject a malicious publish step that:
- Published a trojaned version of the current package to npm
- Read the repository’s OIDC token grants to identify sibling packages in the same GitHub organisation
- Triggered a workflow dispatch on each sibling repository, propagating the worm
The result was a self-replicating supply chain attack that spread across Red Hat’s npm package ecosystem within approximately 90 minutes before containment. Infected package versions were live on npm for between 20 and 110 minutes depending on the package.
The OIDC Token Abuse Mechanism
Modern npm publishing increasingly uses OIDC-based “tokenless” publishing: instead of storing a long-lived npm access token as a CI/CD secret, the workflow requests a short-lived OIDC token from the identity provider (GitHub in this case) and exchanges it for a time-limited npm publish token via npm’s “granular access tokens” endpoint.
This design was intended to eliminate secret sprawl. Miasma turned it into the attack’s propagation engine.
The Exploit Chain
Step 1 — Initial access via workflow injection: TeamPCP compromised a single Red Hat CI/CD workflow file, likely via a dependency that Red Hat’s automation tooling used to validate or lint workflow YAML. The compromised dependency injected an additional step into the workflow.
Step 2 — OIDC token harvest: The injected step requested an OIDC token from GitHub’s token endpoint ($ACTIONS_ID_TOKEN_REQUEST_URL) with the id-token: write permission that was already granted to the workflow for npm publishing. GitHub OIDC tokens include the organisation, repository, and workflow context as claims.
Step 3 — npm publish with malicious payload: Using the harvested OIDC token, the step exchanged it for an npm publish token and published a new patch version of the package containing the worm payload in a postinstall hook.
Step 4 — Lateral spread: The worm’s payload enumerated other repositories in the @redhat-* and redhat-developer GitHub organisation namespaces using the GitHub API (authenticated with the OIDC token’s organisation scope), identified repositories with id-token: write workflow permissions, and triggered workflow_dispatch events on each one — causing each repo’s CI to run the now-infected workflow, restart the cycle, and publish its own infected version.
What the Malicious Payload Did
Installed packages executed the payload via npm’s postinstall lifecycle hook. The hook was a Node.js script that:
- Fingerprinted the environment (OS, CI indicators, cloud metadata endpoints)
- Exfiltrated environment variables to a TeamPCP-controlled endpoint (targeting
AWS_SECRET_ACCESS_KEY,GITHUB_TOKEN,NPM_TOKEN,DOCKER_PASSWORD, and similar patterns) - Attempted to read
.npmrc,.env, and~/.ssh/id_*files - On CI environments, wrote a self-propagation component to the npm global cache to infect future package installs
The payload was obfuscated with a double layer: outer base64 encoding over an inner eval-based string concatenation, designed to defeat static analysis in npm’s automated scanner. The obfuscation signature matched a tool in TeamPCP’s known toolset from a 2025 PyPI campaign.
Affected Packages
Red Hat has published the complete list of 32 affected packages and version numbers in their security advisory (RHSA-npm-2026-0047). Categories of affected packages include:
- OpenShift developer tooling (
@redhat-developer/vscode-openshift-toolsdependencies) - Backstage plugin ecosystem packages maintained by Red Hat
- Red Hat Insights frontend component libraries
- Keycloak JavaScript client packages
All affected packages had the malicious version yanked from npm within 2 hours. npm has revoked all publish tokens associated with the compromised workflows.
Why OIDC-Based Publishing Made This Worse
Paradoxically, the OIDC tokenless publishing design that was supposed to improve security enabled faster lateral spread than a traditional long-lived token attack would have:
- No static secret to revoke: Traditional response to a compromised npm token is to rotate the secret. With OIDC, there was no single token to rotate — each workflow generate its own on demand.
- Organisation-wide scope: GitHub OIDC tokens can be scoped to the organisation, giving the worm visibility into all repos in the org with a single token exchange.
- Automatic re-issuance: Because each workflow run generates a fresh token, the worm could propagate indefinitely across workflow runs without exhausting credentials.
The design was correct in principle — short-lived tokens are better than long-lived secrets. The missing control was restricting id-token: write permission to only the specific workflows and steps that legitimately need it, and auditing what OIDC tokens are being requested and for what scopes.
Defensive Measures
1. Audit OIDC Permissions in Workflows
Review every GitHub Actions workflow that has id-token: write permission. This permission should be scoped as narrowly as possible — ideally to the specific job that performs publishing, not the entire workflow:
jobs:
publish:
permissions:
id-token: write
contents: read
steps:
- name: Publish to npm
# ... only this job gets the permission
Remove id-token: write from any job or workflow that does not explicitly need to publish to npm or authenticate to a cloud provider.
2. Pin Workflow Dependencies
Miasma’s initial access was through a compromised workflow tool. Pin all workflow action references to commit SHAs, not mutable tags:
# Vulnerable -- tag can be changed
- uses: actions/setup-node@v4
# Safe -- SHA is immutable
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
Use tools like Zizmor or StepSecurity’s Harden-Runner to enforce this automatically.
3. Restrict npm Publish Token Scope
When generating npm automation tokens for CI/CD, use the most restrictive granular access token possible:
# Create a package-scoped automation token (npm CLI v9+)
npm token create --type=granular-access \
--packages=@your-org/specific-package \
--expiration=7d \
--cidr=192.168.0.0/24
Do not use org-wide automation tokens that can publish any package in the namespace.
4. Enable npm Package Provenance
npm’s package provenance feature links published packages to a specific GitHub Actions run and commit SHA. Consumers can verify the package came from the expected source:
- name: Publish
run: npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Consumers verifying provenance would have seen that the Miasma-infected versions were published from an unexpected workflow step.
5. Deploy a SAST Step for postinstall Hooks
Scan package.json files for postinstall scripts in your dependency tree. The presence of obfuscated code in a postinstall hook is a strong indicator of supply chain compromise:
# Audit postinstall hooks in your dependency tree
node -e "
const fs = require('fs');
const lock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'));
const pkgs = lock.packages || {};
for (const [name, info] of Object.entries(pkgs)) {
if (info.scripts && info.scripts.postinstall) {
console.log(name, ':', info.scripts.postinstall.substring(0, 100));
}
}
"
Integrate this check into your CI pipeline and fail the build if unexpected postinstall scripts appear.
6. Monitor npm Registry for Your Packages
Set up automated monitoring for new versions of packages you depend on. Unexpected patch releases — especially at unusual hours or from unexpected CI systems — should trigger a review before automatic updates proceed:
# Check recent publish history for a package
npm view @your-org/package time --json | node -e "
const d = require('fs').readFileSync('/dev/stdin','utf8');
const times = JSON.parse(d);
const recent = Object.entries(times)
.sort(([,a],[,b]) => new Date(b)-new Date(a))
.slice(0,5);
recent.forEach(([v,t]) => console.log(v, t));
"
Detection in Your Environment
If you installed any of the 32 affected Red Hat packages during the 2-hour exposure window (approximately June 28, 2026 14:00 — 16:00 UTC), assume the postinstall hook ran. Check for:
- Unexpected outbound connections to
*.teamspace-cdn[.]comormetrics-collect[.]io(TeamPCP infrastructure domains published by npm security team) - New or modified files in
~/.npm/_cache/or%APPDATA%\npm-cache\that contain encoded JavaScript - Exfiltration of CI environment variables — rotate all secrets in any environment where the affected packages were installed