By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.
18px_cookie
e-remove
Blog
Glossary
Customer Story
Video
eBook / Report
Solution Brief

npm is serving malware to 134,000 developers, and the maintainer can’t stop it

An attacker took over the npm account behind react-native-international-phone-number and react-native-country-select, publishing three waves of malicious versions containing malware linked to the Glassworm threat actor.

An attacker took over the npm account behind react-native-international-phone-number and react-native-country-select, publishing three waves of malicious versions containing malware linked to the Glassworm threat actor.

An attacker took over the npm account behind react-native-international-phone-number and react-native-country-select, publishing three waves of malicious versions containing malware linked to the Glassworm threat actor.

Written by
Kiran Raj
Kiran Raj
Published on
March 18, 2026
Updated on
March 18, 2026

An attacker took over the npm account behind react-native-international-phone-number and react-native-country-select, publishing three waves of malicious versions containing malware linked to the Glassworm threat actor.

An attacker took over the npm account behind react-native-international-phone-number and react-native-country-select, publishing three waves of malicious versions containing malware linked to the Glassworm threat actor.

The npm account behind react-native-international-phone-number (92K monthly downloads) and react-native-country-select (42K monthly downloads) has been fully hijacked. The attacker published 3 waves of malicious versions over 3 days, evolving from direct install hooks to a stealth dependency chain that carries no suspicious signals in the parent package. 

As of this writing, “npm install react-native-international-phone-number” and “npm install react-native-country-select” both install malware—and neither has been deprecated. The maintainer appears to be locked out after the attacker changed the account's email. Endor Labs tracked all 11 compromised package versions across the campaign.

Currently, the npm registry confirms that the latest dist-tag for react-native-international-phone-number resolves to the attacker's version:

Figure 1: The npm Versions tab showing v0.12.3 as the current `latest` release. Earlier compromised versions (0.11.8, 0.11.100, 0.11.1000) have been deprecated - but the Wave 3 versions remain live and undeprecated

Compromised package versions

Endor Labs linked and identified a total of 11 compromised package versions across 4 distinct package names:

Package Version Status
react-native-international-phone-number 0.12.1 Live
react-native-international-phone-number 0.12.2 Live
react-native-international-phone-number 0.12.3 (= latest) Live — installs on npm install
react-native-country-select 0.4.1 Live
react-native-country-select 0.4.2 (= latest) Live — installs on npm install
@agnoliaarisian7180/string-argv 0.3.0, 0.3.1 Live
@usebioerhold8733/s-format 2.0.1, 2.0.2, 2.0.3, 2.0.4 Live
react-native-international-phone-number 0.11.8, 0.11.100, 0.11.1000 Deprecated
react-native-country-select 0.3.91 Deprecated

Indicators of Compromise (IoCs)

Endor Labs identified the compromised packages using a combination of file hashes, attacker emails, build-environment fingerprints, Solana wallet address, and dependency chain patterns.

File hashes

File SHA-256
install.js / init.js (malware payload) 59221aa9623d86c930357dba7e3f54138c7ccbd0daa9c483d766cd8ce1b6ad26
package.json with preinstall hook (v0.11.8) 97045e092ec92b8e64e3351f9d1633be9db1c620786bd8d02f308457809569a1
child.js (launcher in s-format@2.0.4) 4285fa5f01eb9c7612c46123dc5582e6109fe5e6b96e0129f42406f0ecda3242

Network indicators

Note: The malware abuses 9 legitimate public Solana RPC endpoints (api.mainnet-beta.solana.com, rpc.ankr.com, shuriken.xyz, getblock.io, p2p.org, and others) as fallback infrastructure for blockchain C2 queries. These are not attacker-controlled and are excluded from the IOC list.

Attacker infrastructure

Indicator Type
6YGcuyFRJKZtcaYCCFba9fScNUvPkGXodXE1mJiSzqDJ Solana C2 wallet
E9vf42zJXFv8Ljop1cG68NAxLDat4ZEGEWDLfJVX38GF Funding wallet
45.32.150.251 Payload server (Vultr, AS20473)
70.34.242.255 C2 IP (Vultr, AS20473)
Email Purpose
voughoeveryc05de@proton.me Hijacked astroonauta npm account (changed from maintainer's Gmail)
usebioerhold8733@proton.me Attacker-created npm account for @usebioerhold8733/s-format
npm Signal Value
Compromised account astroonauta
Attacker-created account usebioerhold8733
Attacker dependency scopes @agnoliaarisian7180, @usebioerhold8733
Build fingerprint Node 22.22.0 / npm 10.9.4

Filesystem artifact

~/init.json — if present, the malware has executed on the system.

Initial discovery

On March 16, react-native-international-phone-number@0.11.8 and react-native-country-select@0.3.91 were published with a preinstall: "node install.js" hook containing a heavily obfuscated payload — a Solana blockchain-based C2 client linked to the GlassWorm threat actor. StepSecurity was first to report and filed GitHub issues in both repositories. The maintainer deprecated both versions and published clean replacements within approximately 90 minutes.

Endor Labs independently flagged both versions as malicious. That should have been the end of the story. 

It wasn't.

Timeline: three waves, widening detection gaps

What followed was a cat-and-mouse game between the attacker, the maintainer, and security scanners — with the attacker consistently publishing during the maintainer's pre-dawn hours (5–6 AM Brazil time), adapting techniques to evade detection, and the maintainer cleaning up each morning. Until Wave 3, when the cleanup stopped and most scanners went blind.

Time (UTC) Event
Wave 1 — Mar 16
Mar 16 10:49 phone-number@0.11.8 published (preinstall + install.js)
Mar 16 10:54 country-select@0.3.91 published (identical payload)
Mar 16 12:53 Maintainer publishes clean v0.11.9 and v0.4.0
Wave 2 — Mar 17
Mar 17 08:23 @usebioerhold8733/s-format@2.0.1 — dep chain pre-staged (init.js missing)
Mar 17 08:44 @agnoliaarisian7180/string-argv@0.3.0 — empty shell package
Mar 17 08:58 phone-number@0.11.100 (preinstall + dep chain)
Mar 17 09:06 phone-number@0.11.1000 (preinstall hook, but install.js missing from tarball)
Mar 17 09:59 Maintainer publishes clean v0.12.0
Wave 3 — Mar 17–18
Mar 17 21:36 phone-number@0.12.1 — account email changed to Proton Mail, maintainer locked out
Mar 17 21:39 country-select@0.4.1 — attacker dep chain added
Mar 18 08:24 s-format@2.0.2init.js payload now present, dependency chain armed
Mar 18 08:43 country-select@0.4.2 — dep chain via "latest", becomes latest dist-tag
Mar 18 08:44 phone-number@0.12.2 — dep chain via "latest", becomes latest dist-tag
Mar 18 09:14 phone-number@0.12.3 — preinstall hook removed, pure stealth, becomes latest
Mar 18+ No maintainer response. Silence.

Endor Labs tracked all 11 compromised package versions across both hijacked packages and the attacker's dependency chain, including the Wave 3 stealth versions.

The infection chain

The malware deployed in this campaign is attributed to the GlassWorm threat actor, sharing infrastructure, obfuscation techniques, and C2 methodology.

How it executes

  1. Entry: preinstall: "node install.js" runs automatically on npm install — no user interaction required
  2. Anti-analysis: 10-second startup delay; skips Russian locale/timezone systems (12 hardcoded timezone checks — a well-known CIS-origin malware technique)
  3. C2 via Solana blockchain: Resolves the payload URL from on-chain transaction memos (see below)
  4. Payload delivery: Fetches the stage-2 payload from an attacker-controlled Vultr server (45.32.150.251); OS type sent via HTTP header for platform-specific payloads; response is AES-256 encrypted with the key and IV delivered in HTTP response headers (secretkey, ivbase64)
  5. In-memory execution: Decrypted payload executed via eval() or vm.Script — never written to disk
  6. Persistence: Writes ~/init.json with a timestamp; won't re-execute within 48 hours on the same machine

Solana blockchain as a censorship-resistant C2

Rather than hardcoding a payload URL — which defenders could blocklist within minutes — the attacker uses the Solana blockchain as a dead-drop C2 channel that no single authority can take down.

The malware polls a hardcoded Solana wallet (6YGcuyFRJKZtcaYCCFba9fScNUvPkGXodXE1mJiSzqDJ) using the getSignaturesForAddress RPC method across 9 public Solana RPC endpoints as fallbacks — all legitimate services (including api.mainnet-beta.solana.com, rpc.ankr.com, shuriken.xyz, and getblock.io). The attacker posts transactions to their own wallet containing Memo Program messages with a base64-encoded payload URL.

This makes the C2 channel effectively unkillable:

  • Blockchain transactions are permanent — they can't be seized, sinkholed, or blocked at the DNS level
  • The attacker can rotate payload servers at any time by posting a single transaction (~$0.001) — no malware update required
  • 9 RPC fallbacks across legitimate providers make network-level blocking impractical
  • The wallet was funded from E9vf42zJXFv8Ljop1cG68NAxLDat4ZEGEWDLfJVX38GF, but Solana wallets are pseudonymous — attribution trails go cold quickly

A deliberate, staged evasion campaign

This was not a smash-and-grab. Each wave was methodically planned to defeat the specific detection that caught the previous one. The attacker studied what got flagged and systematically removed those signals.

Wave-by-wave evasion evolution

Technique Wave 1 Wave 2 Wave 3
preinstall hook in parent Yes Yes Removed
Malware file in parent tarball Yes (install.js) No (missing) No
Dependency chain No Pre-positioned (unarmed — payload withheld) Armed and functional
Dep version pinning N/A Fixed versions (can't be retroactively armed) "latest" (dynamic delivery)
npm account email Maintainer's Gmail Maintainer's Gmail Attacker's Proton Mail

Wave 1 used the most visible technique — a preinstall hook with the malware directly in the tarball. Detected and deprecated within 90 minutes.

Wave 2 pre-staged a dependency chain 35 minutes before publishing the trojanized parent package. Notably, init.js was missing from the deepest dependency (s-format@2.0.1) — and we believe this was intentional, not a deployment error. The parent package (0.11.100) still carried install.js directly as the primary attack vector; the dependency chain was secondary infrastructure being pre-positioned while harmless.

A postinstall hook pointing to a non-existent file looks like sloppy code, not malware, scanners see no payload, nothing to flag. The packages might get indexed as benign. This is a deferred armament technique: plant the dependency chain while it's clean, let scanners clear it, then arm it later when nobody's watching. It took the attacker 23 hours to publish the armed version (s-format@2.0.2) — not because they couldn't iterate faster (they published two parent versions 8 minutes apart), but because the timing was deliberate.

Wave 3 fixed everything:

  • The dependency chain was armed (3 rapid-fire updates to s-format in 38 minutes until functional)
  • Version pins switched to "latest" — making the chain a live weapon that auto-updates
  • The preinstall hook was removed entirely from v0.12.3 — the parent package now contains zero suspicious signals
  • The npm account email was changed, locking the maintainer out of recovery

Account takeover: locking the door behind them

The email change reveals how calculated this campaign was:

Wave npm Account Email Implication
Wave 1 (Mar 16) willian.ralf.rn@gmail.com Attacker has publish access but uses the existing email
Wave 2 (Mar 17 AM) willian.ralf.rn@gmail.com Same — maintainer can still do password resets
Maintainer cleanup (Mar 17 09:59) willian.ralf.rn@gmail.com Maintainer deprecates Wave 2, publishes clean v0.12.0
Wave 3 (Mar 17 21:36) voughoeveryc05de@proton.me Attacker changes email — password resets now go to attacker

The attacker waited until after the maintainer's cleanup to change the email — ensuring the maintainer wouldn't notice during active remediation. The Proton Mail address follows the same pattern as the attacker's dependency chain accounts (@agnoliaarisian7180 and @usebioerhold8733 both use Proton Mail), confirming a single operator.

The email change is visible directly in the npm registry metadata - compare the publisher field between the last clean release and the first Wave 3 release:

Figure 2: npm publisher metadata for v0.12.0 (right, legitimate maintainer's Gmail) vs v0.12.1 (left, attacker's Proton Mail). This is the moment the account was fully hijacked.

The maintainer responded to every previous wave within 1–2 hours. Since Wave 3 — silence. Five compromised versions have sat live on npm for over 24 hours. The attacker didn't just compromise the account; they locked the door behind them.

Using "latest" as a live payload delivery system

The final evolution: dependency packages now pin their versions to "latest" instead of fixed values.

In Wave 2, the attacker used string-argv@0.3.0 → s-format@2.0.1 (fixed versions). When the payload was missing, the chain was permanently broken — unfixable without publishing a new parent version.

In Wave 3, the attacker switched to string-argv@latest → s-format@latest. This changes the game entirely:

  • The attacker can push updated payloads to s-format at any time
  • Every new npm install of the parent package automatically pulls the latest payload
  • No new version of the parent package needs to be published
  • The attacker has a hot-swappable payload delivery pipeline that persists until the dependency packages themselves are taken down

How Endor Labs detected all three waves

When the attacker removed the install hook and buried the payload two layers deep in a dependency chain, surface-level scanning stopped being enough. Endor Labs caught all 11 compromised versions — across 4 package names, 2 hijacked accounts, and 2 attacker-created accounts — by correlating signals across the full dependency tree:

1. SHA256 payload correlation

The malware payload is byte-for-byte identical across every version — whether named install.js or init.js, the SHA256 is always 59221aa9623d86c930357dba7e3f54138c7ccbd0daa9c483d766cd8ce1b6ad26. When s-format@2.0.2 appeared with a file matching this hash, Endor flagged every package that transitively depends on it — even though the parent packages contain zero malicious code themselves. This hash has now appeared in 6 package versions across 3 distinct package names.

2. Transitive dependency chain analysis

Endor resolves the full dependency tree, not just the top-level package.json. 

The Wave 3 attack chain:

react-native-international-phone-number@0.12.3 (no hook, no malicious files)
	→ @agnoliaarisian7180/string-argv@latest (resolves to 0.3.1)
		→ @usebioerhold8733/s-format@latest (resolves to 2.0.4)
			→ postinstall: child.js → exec("node init.js")
				→ init.js = Solana C2 malware (SHA256: 59221aa9...)

Scanners that only inspect the parent package's own files and scripts field see nothing. The malware lives two layers deep.

3. Build-environment fingerprinting

Every attacker-published version shares a build fingerprint that differs from the legitimate maintainer—detectable from registry metadata alone, no file scanning required:

Signal Legitimate Maintainer Attacker
Node.js 16.20.2 / 20.20.0 22.22.0
npm 8.19.4 / 10.8.2 10.9.4
Publisher email willian.ralf.rn@gmail.com voughoeveryc05de@proton.me
gitHead Unique per release Same hash reused across all compromised versions

4. Known-malicious dependency association

The scoped packages @agnoliaarisian7180/* and @usebioerhold8733/* were flagged as malicious in Wave 2. Any new version of any package adding these as dependencies is automatically flagged—which is how v0.12.3 was caught despite having no install hook.

What you should do

  1. Check your lockfiles for any version of react-native-international-phone-number above 0.12.0 or react-native-country-select above 0.4.0
  2. Check for ~/init.json — its presence confirms execution
  3. Rotate all credentials and tokens that were accessible in the environment where the compromised package was installed
  4. Pin to known-clean versions: 0.12.0 for phone-number, 0.4.0 for country-select
  5. Monitor outbound traffic for connections to the Vultr IPs listed above and Solana RPC endpoints

Conclusion

This attack is a case study in how supply chain threats evolve faster than most defenses. The attacker iterated across three waves, systematically removing the signals that triggered detection: first the install hook, then the payload file, then the email trail, and finally the dependency version pins. By Wave 3, the parent package contains no malicious code, suspicious scripts, or visible red flags. The malware executes entirely through a transitive dependency chain that resolves at install time.

Endor Labs caught every wave because detection at this level requires more than scanning the package you install. It requires resolving the full dependency tree, correlating file hashes across packages, tracking build-environment anomalies in registry metadata, and continuously re-evaluating packages when their dependencies change.

The attacker stripped the install hook, buried the malware two dependency layers deep, and pinned everything to "latest" for dynamic delivery. You can't catch what you don't resolve.

Find out More

The Challenge

The Solution

The Impact

Welcome to the resistance
Oops! Something went wrong while submitting the form.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.