critical

CVE

Not assigned

CWE

CWE-506

Affected Surface

  • npm package `@galicia-toolkit-nestjs/paas` versions `999.0.3`, `999.0.4`, `999.0.5`, and `999.0.6`
  • npm package `cloudflare_module` versions `1.0.0`, `1.0.1`, `3.0.0`, and `99.0.1`
  • npm package `kmf-bootstrap` versions `100.100.100`, `100.100.101`, `100.100.102`, `100.100.103`, and `100.100.106`
  • npm package `kmf-vendor-pack` versions `99.0.0`, `100.100.100`, `100.100.101`, and `100.100.106`
  • npm package `ksp-app` version `100.100.100`
  • Developer workstations, CI runners, and internal build systems that could resolve these names from the public npm registry instead of an internal scope or private registry
  • Organizations whose npm, GitHub Actions, AWS, Azure DevOps, or other environment-scoped secrets were present when the affected packages were installed or required

The last three days did not produce one dominant npm compromise on the scale of TanStack or Tensorlake. They produced something smaller and, in some ways, more useful to defenders: a cluster of dependency-confusion packages that show exactly what attackers still expect to learn from a single mistaken registry resolution.

The package names differ, and the infrastructure is not all the same, so it would be sloppy to force them into one attributed campaign. What they do share is a shape:

package name chosen to collide with an internal or plausible dependency
  -> version inflated to beat private or expected semver ranges
  -> install or import path kept quiet enough to avoid breaking builds
  -> host and registry details sent to attacker infrastructure
  -> some variants escalate from metadata beacons to live secret theft

That shape matters because these packages are not trying to behave like real software. They are trying to win dependency resolution, collect proof that they landed inside a private environment, and in some cases steal whatever that environment can already reach.

Confirmed affected packages

PackageConfirmed malicious versionsExecution pointWhat the package exposed
@galicia-toolkit-nestjs/paas999.0.3, 999.0.4, 999.0.5, 999.0.6require() in early versions, then preinstall plus native addon in later onesHostname first, then CI and cloud secrets plus host metadata
cloudflare_module1.0.0, 1.0.1, 3.0.0, 99.0.1postinstall and top-level importHostname, install path, cwd, Node version
kmf-bootstrap100.100.100, 100.100.101, 100.100.102, 100.100.103, 100.100.106postinstallHostname, username, package name, configured npm registry URL
kmf-vendor-pack99.0.0, 100.100.100, 100.100.101, 100.100.106postinstallHostname, username, package name, configured npm registry URL
ksp-app100.100.100postinstallHostname, package name, username, configured npm registry URL

The version numbers are part of the attack. 999.0.x and 100.100.x are not ordinary release lines. They are resolution bait. If an internal package name leaks into a public install path and the client prefers the public registry, these versions are positioned to win.

The clearest escalation path is in @galicia-toolkit-nestjs/paas

The @galicia-toolkit-nestjs/paas line is the strongest technical example in this window because the package family appears to evolve inside a single day.

Public malware reporting describes the early version as a quiet DNS canary. The package exported inert NestJS-shaped stubs, waited a few seconds, then resolved a hostname-derived label under the attacker’s domain. That is enough to answer the operator’s first question: “Did a real environment resolve my fake internal package name?”

The later versions are more aggressive. Their execution path is:

package.json preinstall
  -> node index.js
  -> require("prebuilds/<platform>-<arch>/metrics.node")
  -> native addon forks and detaches
  -> addon reads env vars and host fields
  -> HTTP POST /native to oob.s4yhii.com:9999

The stolen fields are much more interesting than a hostname beacon. Public advisory text names:

AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_SESSION_TOKEN
GITHUB_TOKEN
NPM_TOKEN
NODE_AUTH_TOKEN
SYSTEM_ACCESSTOKEN
ACTIONS_RUNTIME_TOKEN
ACTIONS_ID_TOKEN_REQUEST_TOKEN
AZURE_DEVOPS_EXT_PAT

The same source says the addon also collects gethostname(), getuid(), getpwuid(), getcwd(), and uname(), then packs them into a JSON body with fields such as h, u, os, arch, rel, cwd, and e.

Two details are worth pausing on.

First, the high-signal logic sits in a native addon, not in a long JavaScript file. That makes the package harder to review quickly and easier to miss if a team only scans lifecycle scripts or greps for https.request.

Second, the addon is described as using its own fork() plus raw socket connect() and send() flow rather than Node’s HTTP APIs. That is a deliberate way to bypass simple Node-level network instrumentation.

cloudflare_module is built to stay installed and keep builds green

cloudflare_module is less destructive on paper, but it is a sharper dependency-confusion lure than it first looks.

The package reportedly declares:

"scripts": {
  "postinstall": "node beacon.cjs"
}

Then the module entry point fires the same beacon on import:

require('./beacon.cjs').fire()

The public descriptions say beacon.cjs posts the hostname, install path, current working directory, and Node version to http://185.158.107.175:8787/_ah/dc over plain HTTP.

The clever part is the export surface. Instead of throwing when downstream code touches the package, the module returns a Proxy that yields a no-op for every property access. In other words, the package is designed to fail open:

resolver picks public package
  -> postinstall beacon fires
  -> import-time beacon fires again
  -> bundler still receives a harmless-looking object
  -> build may succeed, so the compromise looks like a normal install

That matters because cloudflare_module is also a real preset name in Nitro and adjacent build tooling. The package is not the preset, but the name collision lowers the chance that a suspicious resolution stands out during review.

kmf-bootstrap, kmf-vendor-pack, and ksp-app focus on one secret many teams forget

The smaller packages in this window are not all trying to steal a full CI credential set. Some are built to answer a narrower question first: which internal registry just resolved me?

The repeated pattern looks like this:

postinstall
  -> load prebuilds/linux-x64/addon.node
  -> read hostname, username, npm_package_name, npm_config_registry
  -> POST to 64.181.165.115/dc

kmf-bootstrap adds a DNS beacon before the raw-socket POST. It reportedly resolves a label under oast.me that includes the host and package name, then sends the same install context to 64.181.165.115 over HTTP/1.0.

kmf-vendor-pack and ksp-app keep the JavaScript layer almost empty. Public reporting describes them as shipping inert module.exports = {} stubs while the real behavior lives in the opaque Linux x64 native addon. That is a good reminder that “the JS file looked empty” is not a meaningful review result when postinstall.js immediately calls require('./prebuilds/linux-x64/addon.node').

The highest-value field in these packages is often not the hostname. It is npm_config_registry.

If a public package can report the registry URL configured on the victim machine, the attacker learns which private package infrastructure is reachable from that environment. That turns one mistaken install into reconnaissance for the next intrusion stage.

The registry already shows the cleanup, not the exposure

Most of these package names have already been unpublished. That is useful evidence for timeline scoping, but it can mislead responders who only check the live registry state.

A clean npm view result today does not answer the important question:

Did any developer workstation, runner, or image resolve one of these names
before npm removed it?

For this cluster, that means checking:

  • lockfiles captured in old commits or CI artifacts
  • package-manager caches on workstations and runners
  • build logs that show npm install, pnpm install, or yarn install
  • internal mirrors or proxies that may have cached the tarballs
  • shell history around emergency version bumps or unpublished-package failures

What to search for

If you want one broad first-pass search across a codebase and home-directory artifacts, start with the package names:

rg -n \
  '@galicia-toolkit-nestjs/paas|cloudflare_module|kmf-bootstrap|kmf-vendor-pack|ksp-app' \
  package.json package-lock.json pnpm-lock.yaml yarn.lock bun.lock .npmrc "$HOME" .

Then look for the install-time and network markers called out by the public advisories:

rg -n \
  'metrics\\.node|beacon\\.cjs|postinstall\\.js|oob\\.s4yhii\\.com|185\\.158\\.107\\.175|64\\.181\\.165\\.115|oast\\.me|oast\\.pro|npm_config_registry' \
  "$HOME" . 2>/dev/null

If you operate CI, search the workflow and runner side for the specific secret names the stronger package targeted:

rg -n \
  'NPM_TOKEN|NODE_AUTH_TOKEN|GITHUB_TOKEN|ACTIONS_RUNTIME_TOKEN|ACTIONS_ID_TOKEN_REQUEST_TOKEN|AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|SYSTEM_ACCESSTOKEN|AZURE_DEVOPS_EXT_PAT' \
  .github .gitlab-ci.yml azure-pipelines.yml Jenkinsfile .circleci . 2>/dev/null

That last search is not an IOC search by itself. It is scope discovery. If those secrets were available on the same machine that installed @galicia-toolkit-nestjs/paas, the event is no longer just a dependency mix-up.

Response guidance

The safe default for this cluster is to treat a confirmed install as host compromise.

  1. Isolate the affected workstation, runner, or build image.
  2. Rotate any npm, GitHub, cloud, CI, or internal-registry credentials that were present on that machine.
  3. Review internal registry and package-proxy logs for other names the same environment may have resolved from the public registry.
  4. Rebuild the environment from a known-good image if the package reached install or import time.
  5. Tighten registry scoping so internal names cannot silently fall through to the public npm registry.

The larger lesson is plain. These October packages did not need zero-days, flashy obfuscation, or long dwell time. They only needed one public resolution of a package name a private environment expected to keep private. Once that happened, even a package whose JavaScript looked empty could still tell an attacker where your registry lives, what host installed it, and whether your runner was carrying secrets worth stealing.

From research to remediation

Check whether this pattern exists in your codebase

Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.

References