Microsoft-tracked npm dependency-confusion developer-profiling campaign
Microsoft attributed a 33-package npm dependency-confusion campaign to shared postinstall tradecraft that profiled developer environments, ran in reconnaissance-only mode, and beaconed to a shared command-and-control endpoint.
On this page 0% read
Executive Summary
Microsoft reported a dependency-confusion campaign that spread across multiple npm organizational scopes and used malicious postinstall payloads to profile developer environments during installation. The payloads collected host and environment metadata, ran in a reconnaissance-only mode, and used a shared command-and-control path with a common X-Secret header. The public article names 33 malicious packages overall, but the full package set is not exhaustively enumerated in the HTML, so this report preserves that uncertainty while documenting the directly observed artifacts.
The three registry artifacts in the candidate set all resolve to the same security-holding 0.0.1-security release, which makes them useful anchors for hunting in lockfiles, package caches, build logs, and exported telemetry. The operational pattern is most relevant to developer endpoints, CI runners, and any environment where npm, Node.js, or Bun package installation can execute lifecycle hooks.
Key Facts
- Campaign type: npm dependency confusion with developer profiling and postinstall execution
- Reported scale: 33 malicious packages across multiple organizational scopes [1]
- Known packages in this candidate set:
@cloudplatform-single-spa/logaas,@capibar.chat/ui-kit,@sber-ecom-core/sberpay-widget - Known malicious versions:
0.0.1-securityfor the three registry artifacts [2] [3] [4] - Behavioral indicators:
postinstall,RECON_ONLY,X-Secret, and beaconing tooob[.]moika[.]tech - Primary exposure surface: developer workstations, CI runners, and build environments that install dependencies from npm
- Collection gap: the public article indicates a broader cluster, but not every package artifact is surfaced in the HTML
Evidence Assessment
| Claim | Status | Evidence |
|---|---|---|
| Microsoft tracked a 33-package npm dependency-confusion campaign that profiled developer environments. | confirmed | The Microsoft article title and body describe the campaign at scale and its focus on profiling developer systems [1]. |
| The three registry artifacts resolve to a shared security-holding version. | confirmed | Each registry endpoint shows 0.0.1-security as the published version [2] [3] [4]. |
| The payload runs during installation and uses a recon-only design. | confirmed | Microsoft describes a postinstall stager, a reconnaissance-only mode, and a server-side RECON_ONLY flag [1]. |
The campaign uses shared infrastructure and a common X-Secret header. | confirmed | Microsoft documents a shared C2 endpoint and a common X-Secret header across the cluster [1]. |
| The public HTML exposes the complete package list. | not_observed | Microsoft describes the larger cluster, but the HTML available here does not fully enumerate every malicious registry artifact [1]. |
Impact Determination
| Classification | Criteria | Handling |
|---|---|---|
| Confirmed compromise | An affected package/version is installed and telemetry shows postinstall execution, outbound beaconing, or the X-Secret / RECON_ONLY indicators. | Isolate the host or runner, preserve artifacts, and rotate reachable credentials from a clean system. |
| Presumed exposed | An affected package/version resolved in a build or workstation, but runtime telemetry is incomplete. | Treat exposed credentials as compromised until clean rebuilds and log review complete. |
| Potentially exposed | Repositories or builds reference the package names, but exact resolution is unclear. | Reconstruct lockfiles, caches, and install logs before narrowing scope. |
| Not exposed | Complete evidence sets show none of the package names, versions, or behavioral indicators. | Preserve negative evidence and keep lifecycle-script controls in place. |
| Unknown | Dependency inventory or telemetry is missing. | Keep credentials in scope until evidence is recovered or the risk owner accepts residual uncertainty. |
Minimum Evidence To Collect
- Lockfiles and package caches because they prove whether the malicious version resolved on the machine, which is the fastest way to confirm exposure for npm, Bun, or CI installs.
- Package manager logs and shell history because they can show the actual install command, the lifecycle hook invocation, and whether the host used
npm installor another resolver. - Endpoint process telemetry because the campaign depends on install-time execution, so a
nodeorbunchild process can turn an ambiguous package hit into a confirmed execution path. - Proxy, DNS, and EDR network logs because they can reveal the outbound beaconing pattern to
oob[.]moika[.]techand any request carrying the sharedX-Secretheader. - CI job logs and workflow artifacts because build systems are the likely place where package resolution, lifecycle hooks, and credential reachability intersect.
Timeline
- 2026-05-29: Microsoft publishes the article describing the campaign, its shared infrastructure, and the developer-profiling tradecraft [1].
- 2026-05-29: The three registry endpoints in this candidate set resolve to the
0.0.1-securityrelease [2] [3] [4]. - 2026-05-29: This report consolidates the three registry artifacts with the broader Microsoft-documented cluster and preserves the remaining enumeration gap.
What Happened
Microsoft describes a two-burst dependency-confusion campaign that used malicious npm packages to execute a postinstall stager during dependency installation. The stager ran in a recon-only posture, gathered system information, hostnames, environment variables, and developer context, and then beaconed outward using a shared infrastructure pattern [1].
The article also notes that the packages impersonated internal enterprise dependencies across multiple organizational scopes. That matters operationally because the attack does not need a production server compromise to succeed; a single developer workstation or CI runner can expose npm tokens, GitHub credentials, cloud credentials, and other secrets reachable from the installation environment [1].
Technical Analysis
The campaign is technically significant because it combines dependency confusion with install-time execution and post-exploitation readiness. Microsoft describes the payload as a heavily obfuscated postinstall stager that runs silently during npm install, gathers developer profiling data, and uses a shared RECON_ONLY flag that can be switched server-side for follow-on exploitation [1]. That means the malicious behavior is not limited to a one-time fetch: the same infrastructure can support staged escalation if exposed environments are useful enough to revisit.
The shared X-Secret header and the common oob[.]moika[.]tech endpoint provide strong hunting handles. Microsoft’s article attributes the same operator tradecraft across multiple organizational scopes, and the public HTML shows package families such as @cloudplatform-single-spa/logaas, @capibar.chat/ui-kit, and @sber-ecom-core/sberpay-widget among the broader set [1]. The registry endpoints for those three packages all report the same 0.0.1-security release, which makes them especially important for lockfile, cache, and artifact triage [2] [3] [4].
A practical reading of the campaign is that a single install on a workstation or runner can be enough to expose more than one credential class. Even if the operator only wants reconnaissance first, the profile data can later steer targeted credential theft or cloud abuse from a more valuable environment [1].
Affected Assets and Blast Radius
The machine-readable profile keeps the package names canonical and unversioned in affected_assets.packages, while version-specific exposure is recorded separately in iocs.package_versions. That split matters because the broader cluster is still incompletely enumerated in public HTML, but the known artifacts are enough to support targeted hunting.
The most likely blast radius includes:
- developer laptops with npm, Node.js, or Bun installed
- CI runners that execute package lifecycle scripts
- build containers that cache npm installs or preserve workspace artifacts
- release engineering systems that hold publishing tokens, cloud keys, or GitHub credentials
Because the payload is install-time and reconnaissance-driven, the blast radius should not be constrained to source repositories alone. Package caches, logs, and ephemeral build workspaces are just as important as checked-in code.
Indicators of Compromise
Package Versions
@cloudplatform-single-spa/logaas@0.0.1-security@capibar.chat/ui-kit@0.0.1-security@sber-ecom-core/sberpay-widget@0.0.1-security
Package Names
@capibar.chat/ui-kit@ce-rwb/ce-tools-editor-admin@ce-rwb/ce-tools-editor-core@ce-rwb/ce-tools-editor-render@cloudplatform-single-spa/logaas@data-science/llm@payments-widget/payments-widget-sdk@sber-ecom-core/sberpay-widget@t-in-one/add_app_middleware_token@t-in-one/add_application@t-in-one/add_application_service_token@t-in-one/add_application_tid@t-in-one/application_id_storage_key_token@t-in-one/form_product_token@t-in-one/get_application_hid@t-in-one/only_difference_payload@t-in-one/prefill_bundle_data_token@t-in-one/prefill_credit_data_token@travel-autotests/npm-proto@wb-track/shared-front@wordpress/interactivity@wordpress/interactivity-js-modulepreload
Files
package.jsonpackage-lock.jsonpnpm-lock.yamlyarn.lockbun.lockscripts/postinstall.jsscripts/install.js.github/workflows/*.yml.github/workflows/*.yaml
Network and Behavioral Indicators
oob[.]moika[.]techX-SecretRECON_ONLYpostinstallnpm installdeveloper environment fingerprintingcredential reconnaissance
Detection and Hunting
Hunt Manifest: npm-dependency-confusion-developer-profiling-hunt-1
- Title: workspace and telemetry scan for Microsoft-tracked dependency-confusion indicators
- Question: Does the checkout or exported telemetry contain the npm dependency-confusion campaign indicators documented by Microsoft?
- Telemetry Family: process
- Telemetry Context: repository checkout, package cache, or CI/exported log directory
- Positive Signal: Matched any of the affected npm scopes, representative package names, version markers, oob.moika.tech, X-Secret, or RECON_ONLY markers
- False Positives: Legitimate internal package documentation may mention the same scope names; require package-cache or install telemetry before escalating.
- Classification on Match: presumed_exposed
#!/usr/bin/env python3
"""Hunt for Microsoft-tracked npm dependency-confusion developer-profiling indicators."""
import argparse
import json
import os
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Iterable
SLUG = "npm-dependency-confusion-developer-profiling"
PACKAGE_NAMES = [
"@capibar.chat/ui-kit",
"@ce-rwb/ce-tools-editor-admin",
"@ce-rwb/ce-tools-editor-core",
"@ce-rwb/ce-tools-editor-render",
"@cloudplatform-single-spa/logaas",
"@data-science/llm",
"@payments-widget/payments-widget-sdk",
"@sber-ecom-core/sberpay-widget",
"@t-in-one/add_app_middleware_token",
"@t-in-one/add_application",
"@t-in-one/add_application_service_token",
"@t-in-one/add_application_tid",
"@t-in-one/application_id_storage_key_token",
"@t-in-one/form_product_token",
"@t-in-one/get_application_hid",
"@t-in-one/only_difference_payload",
"@t-in-one/prefill_bundle_data_token",
"@t-in-one/prefill_credit_data_token",
"@travel-autotests/npm-proto",
"@wb-track/shared-front",
"@wordpress/interactivity",
"@wordpress/interactivity-js-modulepreload",
]
PACKAGE_VERSIONS = [
"@capibar.chat/ui-kit@0.0.1-security",
"@ce-rwb/ce-tools-editor-admin@0.0.1-security",
"@ce-rwb/ce-tools-editor-core@0.0.1-security",
"@ce-rwb/ce-tools-editor-render@0.0.1-security",
"@cloudplatform-single-spa/logaas@0.0.1-security",
"@data-science/llm@0.0.1-security",
"@payments-widget/payments-widget-sdk@0.0.1-security",
"@sber-ecom-core/sberpay-widget@0.0.1-security",
"@t-in-one/add_app_middleware_token@0.0.1-security",
"@t-in-one/add_application@0.0.1-security",
"@t-in-one/add_application_service_token@0.0.1-security",
"@t-in-one/add_application_tid@0.0.1-security",
"@t-in-one/application_id_storage_key_token@0.0.1-security",
"@t-in-one/form_product_token@0.0.1-security",
"@t-in-one/get_application_hid@0.0.1-security",
"@t-in-one/only_difference_payload@0.0.1-security",
"@t-in-one/prefill_bundle_data_token@0.0.1-security",
"@t-in-one/prefill_credit_data_token@0.0.1-security",
"@travel-autotests/npm-proto@0.0.1-security",
"@wb-track/shared-front@0.0.1-security",
"@wordpress/interactivity@0.0.1-security",
"@wordpress/interactivity-js-modulepreload@0.0.1-security",
]
DOMAINS = ["oob.moika.tech"]
HEADERS = ["X-Secret"]
FLAGS = ["RECON_ONLY"]
PROCESS_TERMS = [
"postinstall",
"npm lifecycle hook",
"dependency confusion",
"developer environment fingerprinting",
"credential reconnaissance",
"developer context",
"environment variables",
"hostname",
]
EXCLUDED_DIRS = {".git", "node_modules", "dist", "build", "coverage", ".cache", ".next", ".turbo", "vendor", "tmp", "temp"}
BINARY_SUFFIXES = {".png", ".jpg", ".jpeg", ".gif", ".ico", ".pdf", ".zip", ".gz", ".tgz", ".bz2", ".xz", ".7z", ".rar", ".exe", ".dll", ".so", ".dylib", ".class", ".jar"}
@dataclass(frozen=True)
class Finding:
path: str
line: int
category: str
indicator: str
evidence: str
INDICATORS: list[tuple[str, str]] = []
for value in PACKAGE_VERSIONS:
INDICATORS.append(("package_version", value))
for value in PACKAGE_NAMES:
INDICATORS.append(("package", value))
for value in DOMAINS:
INDICATORS.append(("domain", value))
for value in HEADERS:
INDICATORS.append(("header", value))
for value in FLAGS:
INDICATORS.append(("flag", value))
for value in PROCESS_TERMS:
INDICATORS.append(("process", value))
def _read_text(path: Path) -> str | None:
if path.suffix.lower() in BINARY_SUFFIXES:
return None
try:
return path.read_text(encoding="utf-8", errors="ignore")
except (OSError, UnicodeError):
return None
def _iter_files(roots: Iterable[Path]) -> Iterable[Path]:
for root in roots:
if not root.exists():
continue
if root.is_file():
yield root
continue
for current, dirs, files in os.walk(root):
dirs[:] = [d for d in dirs if d not in EXCLUDED_DIRS]
for filename in files:
yield Path(current) / filename
def _first_line_with(text: str, needle: str) -> tuple[int, str]:
needle_lower = needle.lower()
for idx, line in enumerate(text.splitlines(), start=1):
if needle_lower in line.lower():
return idx, line.strip()
return 1, text[:200].strip().replace("\n", " ")
def scan_paths(paths: Iterable[Path]) -> list[Finding]:
findings: list[Finding] = []
seen: set[tuple[str, str, str]] = set()
for path in _iter_files(paths):
text = _read_text(path)
if not text:
continue
lowered = text.lower()
for category, indicator in INDICATORS:
needle = indicator.lower()
if needle in lowered:
line, evidence = _first_line_with(text, indicator)
key = (str(path), category, indicator)
if key in seen:
continue
seen.add(key)
findings.append(
Finding(
path=str(path),
line=line,
category=category,
indicator=indicator,
evidence=evidence,
)
)
findings.sort(key=lambda item: (item.path, item.line, item.category, item.indicator))
return findings
def _build_report(findings: list[Finding]) -> str:
if not findings:
return "No Microsoft-tracked dependency-confusion indicators found."
lines = [f"{len(findings)} indicator hit(s) across {len({f.path for f in findings})} file(s):"]
for finding in findings:
lines.append(
f"- {finding.path}:{finding.line} [{finding.category}] {finding.indicator} :: {finding.evidence}"
)
return "\n".join(lines)
def _write_output(output_path: Path, findings: list[Finding]) -> None:
payload = {
"slug": SLUG,
"finding_count": len(findings),
"findings": [asdict(finding) for finding in findings],
}
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("paths", nargs="*", default=["."], help="Repository or log export paths to scan")
parser.add_argument("--json", action="store_true", help="Emit JSON to stdout")
parser.add_argument("--output", type=Path, help="Optional path to write a JSON report")
args = parser.parse_args(argv)
roots = [Path(path) for path in args.paths]
findings = scan_paths(roots)
if args.output:
_write_output(args.output, findings)
if args.json:
print(json.dumps({"slug": SLUG, "finding_count": len(findings), "findings": [asdict(f) for f in findings]}, indent=2, sort_keys=True))
else:
print(_build_report(findings))
return 1 if findings else 0
if __name__ == "__main__":
raise SystemExit(main())
Downstream Abuse Audits
After a positive hit, review the following for follow-on abuse:
- npm token usage and registry publish history
- GitHub Actions or repository secrets reachable from the environment
- cloud access keys, OIDC tokens, and deployment credentials
- package caches, container layers, and CI workspaces that may still contain the malicious tarball or the postinstall artifact
- outbound requests from developer or runner hosts that reference
oob[.]moika[.]techor carry the shared header
If the host or runner had access to production-adjacent secrets, treat the event as credential exposure even if you do not yet have a complete process trace.
Remediation and Closure
- Remove the affected package versions from active dependency graphs and rebuild from a clean source state.
- Rotate any credentials that were reachable from the installed environment, including npm, GitHub, cloud, and deployment secrets.
- Purge package caches, build workspaces, and ephemeral CI artifacts that may still contain the malicious release or its outputs.
- Add lifecycle-script restrictions, provenance checks, and tighter dependency-confusion controls for developer workstations and CI runners.
- Close the incident only after lockfiles, caches, and telemetry confirm that the affected package versions did not execute or that all reachable credentials were rotated.
Sources
IOC Clipboard
4 IOCscapibar.chat capibar[.]chat yarn.lock yarn[.]lock bun.lock bun[.]lock oob.moika.tech oob[.]moika[.]tech