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.
| Package | Malicious version(s) | OSV |
|---|---|---|
google-cloud-internal-build-helper | 1.2.45 | MAL-2026-15839 |
google-cloud-internal-core-utils | 1.2.50 | MAL-2026-15840 |
google-cloud-mono-repo-helper | 1.0.1777211772 | MAL-2026-15841 |
google-internal-cloud-audit-security-check | 99.9.1777210552, 99.9.1777210553 | MAL-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:
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.
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:
- Whether the trigger was
preinstall,postinstall,install, or import-time - Whether the payload was a stealer, backdoor, dropper, RAT, or combination
- Any C2 host, IP, URL, or port
- Which secrets or files were targeted
- Whether any persistence survived package removal
- Whether the five malicious versions carried different payloads
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.
| Order | Package | Version | Publish (UTC) | Delta from first |
|---|---|---|---|---|
| 1 | google-internal-cloud-audit-security-check | 99.9.1777210552 | 2026-04-26T13:36:02.254Z | +0s |
| 2 | google-internal-cloud-audit-security-check | 99.9.1777210553 | 2026-04-26T13:49:44.117Z | +821.9s |
| 3 | google-cloud-mono-repo-helper | 1.0.1777211772 | 2026-04-26T13:57:15.897Z | +1273.6s |
| 4 | google-cloud-internal-build-helper | 1.2.45 | 2026-04-26T14:00:53.599Z | +1491.3s |
| 5 | google-cloud-internal-core-utils | 1.2.50 | 2026-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
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/nullnpm ls google-cloud-internal-build-helper google-cloud-internal-core-utils google-cloud-mono-repo-helper google-internal-cloud-audit-security-check 2>/dev/nullIf either command finds a hit:
- 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) - Remove the package from manifests and lockfiles, then reinstall from known-good sources
- Review CI history for the 129-day window starting 2026-04-26 for any resolve against public npm for these names
- Check private registry configuration so internal unscoped names cannot fall through to the public registry
- Hunt for adjacent
google-cloud-internal-*orgoogle-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
| Indicator | Type | Value | Method |
|---|---|---|---|
google-cloud-internal-build-helper | npm package | 1.2.45 | Registry time field; OSV MAL-2026-15839 |
google-cloud-internal-core-utils | npm package | 1.2.50 | Registry time field; OSV MAL-2026-15840 |
google-cloud-mono-repo-helper | npm package | 1.0.1777211772 | Registry time field; OSV MAL-2026-15841 |
google-internal-cloud-audit-security-check | npm package | 99.9.1777210552, 99.9.1777210553 | Registry time field; OSV MAL-2026-15842 |
| GHSA aliases | Advisory IDs | GHSA-3qp7-c8c6-w6vj, GHSA-4vvq-8686-f44c, GHSA-8rcc-3mvv-4c2x, GHSA-cw6m-gmx9-x556 | GHSA / OSV |
| Detection source | Advisory source | ghsa-malware | OSV / OSSF |
| CWE | Classification | CWE-506 | OSV advisory |
| Security placeholder | npm version | 0.0.1-security (all four) | npm registry; published 2026-09-03 |
| Epoch patches | Version encoding | 1777211772, 1777210552, 1777210553 | Decoded vs registry timestamps |
| Publisher | Identity | Stripped / empty | npm 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
| Package | Version | Published (UTC) | Status | OSV |
|---|---|---|---|---|
google-cloud-internal-build-helper | 1.2.45 | 2026-04-26T14:00:53Z | Replaced by 0.0.1-security | MAL-2026-15839 |
google-cloud-internal-core-utils | 1.2.50 | 2026-04-26T14:05:16Z | Replaced by 0.0.1-security | MAL-2026-15840 |
google-cloud-mono-repo-helper | 1.0.1777211772 | 2026-04-26T13:57:15Z | Replaced by 0.0.1-security | MAL-2026-15841 |
google-internal-cloud-audit-security-check | 99.9.1777210552 | 2026-04-26T13:36:02Z | Replaced by 0.0.1-security | MAL-2026-15842 |
google-internal-cloud-audit-security-check | 99.9.1777210553 | 2026-04-26T13:49:44Z | Replaced by 0.0.1-security | MAL-2026-15842 |
GHSA pages: GHSA-3qp7-c8c6-w6vj, GHSA-4vvq-8686-f44c, GHSA-8rcc-3mvv-4c2x, GHSA-cw6m-gmx9-x556.
