CentrioleBlog
Back to blog

Threat Research

129 Days on Public npm: google-cloud-internal Packages Built for Dependency Confusion

Four unscoped packages named like Google Cloud internal tooling sat on npm for 129 days, published with three different dependency confusion version strategies, before npm replaced them with security placeholders.

Date

Reading time

9 min read

Author

Centriole Research
Share
129 Days on Public npm: google-cloud-internal Packages Built for Dependency Confusion

Four packages with internal in the name sat on the public npm registry from April 26 to September 3, 2026. That is 129 days, 13 hours, and 9 minutes between the first malicious publish and npm’s security placeholder. The names look like private Google Cloud tooling. The version numbers look like three different dependency confusion strategies in one sitting.

GitHub’s automated malware scanner filed the four GHSA records in the same second on September 3. Advisories from that source use a standard “fully compromised” template and do not include payload extracts or C2 indicators. The registry time field still keeps the original publish stamps, and that is where the forensic story sits.

Registry findings

Google’s public Node.js SDK packages live under scoped names such as @google-cloud/storage and @google-cloud/bigquery, published through the google-wombot account. These four packages are unscoped. They read like the private helpers that would only exist on an internal registry: build utilities, core utils, a mono-repo helper, and a security audit check.

Four unscoped google-cloud-internal packages impersonating private Google Cloud tooling
Four unscoped google-cloud-internal packages impersonating private Google Cloud tooling
PackageMalicious version(s)OSV
google-cloud-internal-build-helper1.2.45MAL-2026-15839
google-cloud-internal-core-utils1.2.50MAL-2026-15840
google-cloud-mono-repo-helper1.0.1777211772MAL-2026-15841
google-internal-cloud-audit-security-check99.9.1777210552, 99.9.1777210553MAL-2026-15842

The audit package flips the word order to google-internal-cloud-*. The other three stay on google-cloud-internal-*. That is still the same naming family: unscoped, Google Cloud flavored, and built around the word internal.

npm’s security team replaced all four packages with 0.0.1-security holders as part of the takedown. Malicious version metadata is stripped from the versions object, and the original publisher account is removed from the record. The _rev counts still line up with the publish history: three packages at revision 3 (one malicious version each), and the audit package at revision 4 (two malicious versions plus the security replacement).

We checked deps.dev, npm dependent search, and GitHub code search for these names in package.json files. Public dependents came back empty. That does not prove nobody installed them. A private CI pipeline that resolved against public npm would leave no public graph.

Version strategies

Dependency confusion does not need a typo. If a private registry already holds a package named google-cloud-internal-build-helper at 1.2.44, and public npm has 1.2.45, a misconfigured resolver can prefer the higher public version. The install looks normal. The payload runs under the cover name the org already trusts.

This cluster did not pick one version pattern and repeat it. In a single 29-minute session the operator used three:

Three dependency confusion version strategies used in one publish session
Three dependency confusion version strategies used in one publish session

Maximum blast (v99.9.x). google-internal-cloud-audit-security-check shipped 99.9.1777210552 and 99.9.1777210553. Major 99 beats any internal package that stays in a sane 1.x through 98.x range. That is the nuclear option for confusion.

Epoch patch (v1.0.epoch). google-cloud-mono-repo-helper shipped 1.0.1777211772. Major and minor stay at 1.0. The patch is a Unix epoch. That only wins against internal packages still on the 1.0.x line. Organizations on 1.1.x or 2.x would not be hit by this version alone.

Specific 1.2.x targeting. google-cloud-internal-build-helper at 1.2.45 and google-cloud-internal-core-utils at 1.2.50 look like ordinary development versions. They only beat lower 1.2.x lines. The five-patch gap between 45 and 50 may mean nothing, or it may track different internal histories. We cannot tell from public data whether those numbers were reconnaissance-informed or just plausible picks.

Epoch encoding

Two of the packages embed Unix time in the patch field. We decoded those values against the registry publish stamps.

Unix epoch timestamps embedded in semver patch components
Unix epoch timestamps embedded in semver patch components

1777211772 on google-cloud-mono-repo-helper decodes to 2026-04-26T13:56:12Z. npm recorded the publish at 13:57:15Z, 63 seconds later. 1777210552 on the first audit version decodes to 13:35:52Z, with publish at 13:36:02Z, a 10-second gap. Both are consistent with an automation clock that stamps the version string just before the registry accepts the upload.

The second audit version is different. The patch is 1777210553, exactly one second after the first epoch, but the publish lands at 13:49:44Z, 13.7 minutes later. A fresh epoch generator at that wall clock would have produced something near 1777211344. The operator (or a script) incremented the previous epoch by 1 instead. Possible reasons include a broken first payload, a reinstall test under a caret range, a rate-limit retry, or a manual bump without regenerating the clock. The registry alone cannot choose among those.

build-helper and core-utils have no epoch encoding. Their 1.2.x numbers are ordinary semver.

Advisory scope on the payload

The public record for these packages is registry metadata plus GHSA classification. npm’s takedown removed the malicious releases from the downloadable package set, and the GHSA text is identical across all four advisories. It classifies the case and states impact, without describing install hooks, exfil paths, or infrastructure:

Any computer that has this package installed or running should be considered fully compromised. All secrets and keys stored on that computer should be rotated immediately from a different computer.

CWE-506 (Embedded Malicious Code) applies. Affected ranges start at introduced: 0. There is no patched version.

Those advisories do not document:

The “full control” language is stronger than the wording often used for a simple install-time env stealer. Treat that as how GHSA classified the case. Do not read it as a confirmed implant layout beyond the advisory text.

Publish timestamps

All five malicious versions landed on April 26, 2026, inside a 29.2-minute window. The order is not alphabetical and not the OSV ID order.

Publish session on April 26 and 129-day detection gap until September 3
Publish session on April 26 and 129-day detection gap until September 3
OrderPackageVersionPublish (UTC)Delta from first
1google-internal-cloud-audit-security-check99.9.17772105522026-04-26T13:36:02.254Z+0s
2google-internal-cloud-audit-security-check99.9.17772105532026-04-26T13:49:44.117Z+821.9s
3google-cloud-mono-repo-helper1.0.17772117722026-04-26T13:57:15.897Z+1273.6s
4google-cloud-internal-build-helper1.2.452026-04-26T14:00:53.599Z+1491.3s
5google-cloud-internal-core-utils1.2.502026-04-26T14:05:16.832Z+1754.6s

Intervals were irregular: 13.7 minutes, then about 7.5, 3.6, and 4.4 minutes. That is slower and less uniform than a tight scripted burst. It fits a human operator publishing manually or semi-manually, an automation with deliberate delays, or a mix of both.

npm replaced the packages with 0.0.1-security on September 3 starting at 02:45:08Z, about four seconds apart in sequence. GHSA advisories were published at 02:46:19Z, 70 seconds after the first placeholder. OSSF imported the batch at 05:28:34Z. The source on every advisory is ghsa-malware, not a named human reporter.

Compared with same-day alert traffic for other clusters we covered, this dwell is the outlier. Those other sets were published and caught within hours or minutes. These four names waited over four months. Specific internal-looking names are less likely to be installed by accident than a typosquat of a popular library, and empty public dependents mean there may have been little victim telemetry to wake a scanner earlier.

If you installed these packages

Lockfile and manifest scan
rg -n "google-cloud-internal-build-helper|google-cloud-internal-core-utils|google-cloud-mono-repo-helper|google-internal-cloud-audit-security-check" package.json package-lock.json yarn.lock pnpm-lock.yaml npm-shrinkwrap.json 2>/dev/null
Installed package check
npm ls google-cloud-internal-build-helper google-cloud-internal-core-utils google-cloud-mono-repo-helper google-internal-cloud-audit-security-check 2>/dev/null

If either command finds a hit:

  1. Isolate the host from a clean machine and rotate credentials that process could reach (cloud keys, npm tokens, SSH keys, CI secrets, .npmrc, and related env)
  2. Remove the package from manifests and lockfiles, then reinstall from known-good sources
  3. Review CI history for the 129-day window starting 2026-04-26 for any resolve against public npm for these names
  4. Check private registry configuration so internal unscoped names cannot fall through to the public registry
  5. Hunt for adjacent google-cloud-internal-* or google-internal-cloud-* names that may still be unpublished siblings

Do not assume a clean history because npm now serves 0.0.1-security. Cached mirrors, offline tarballs, and private proxies can keep the malicious versions reachable after a public takedown.

Indicators

IndicatorTypeValueMethod
google-cloud-internal-build-helpernpm package1.2.45Registry time field; OSV MAL-2026-15839
google-cloud-internal-core-utilsnpm package1.2.50Registry time field; OSV MAL-2026-15840
google-cloud-mono-repo-helpernpm package1.0.1777211772Registry time field; OSV MAL-2026-15841
google-internal-cloud-audit-security-checknpm package99.9.1777210552, 99.9.1777210553Registry time field; OSV MAL-2026-15842
GHSA aliasesAdvisory IDsGHSA-3qp7-c8c6-w6vj, GHSA-4vvq-8686-f44c, GHSA-8rcc-3mvv-4c2x, GHSA-cw6m-gmx9-x556GHSA / OSV
Detection sourceAdvisory sourceghsa-malwareOSV / OSSF
CWEClassificationCWE-506OSV advisory
Security placeholdernpm version0.0.1-security (all four)npm registry; published 2026-09-03
Epoch patchesVersion encoding1777211772, 1777210552, 1777210553Decoded vs registry timestamps
PublisherIdentityStripped / emptynpm security replacement

OSSF SHA256 values attached to these advisories hash the advisory documents, not the package tarballs. No package content hashes are available from the GHSA source.

Affected versions

PackageVersionPublished (UTC)StatusOSV
google-cloud-internal-build-helper1.2.452026-04-26T14:00:53ZReplaced by 0.0.1-securityMAL-2026-15839
google-cloud-internal-core-utils1.2.502026-04-26T14:05:16ZReplaced by 0.0.1-securityMAL-2026-15840
google-cloud-mono-repo-helper1.0.17772117722026-04-26T13:57:15ZReplaced by 0.0.1-securityMAL-2026-15841
google-internal-cloud-audit-security-check99.9.17772105522026-04-26T13:36:02ZReplaced by 0.0.1-securityMAL-2026-15842
google-internal-cloud-audit-security-check99.9.17772105532026-04-26T13:49:44ZReplaced by 0.0.1-securityMAL-2026-15842

GHSA pages: GHSA-3qp7-c8c6-w6vj, GHSA-4vvq-8686-f44c, GHSA-8rcc-3mvv-4c2x, GHSA-cw6m-gmx9-x556.