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:

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
| 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) | ||
| 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.2 — init.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
- Entry:
preinstall: "node install.js"runs automatically on npm install — no user interaction required - Anti-analysis: 10-second startup delay; skips Russian locale/timezone systems (12 hardcoded timezone checks — a well-known CIS-origin malware technique)
- C2 via Solana blockchain: Resolves the payload URL from on-chain transaction memos (see below)
- 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) - In-memory execution: Decrypted payload executed via
eval()orvm.Script— never written to disk - Persistence: Writes
~/init.jsonwith 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-formatin 38 minutes until functional) - Version pins switched to "
latest"— making the chain a live weapon that auto-updates - The
preinstallhook was removed entirely fromv0.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:

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
- Check your lockfiles for any version of
react-native-international-phone-numberabove0.12.0orreact-native-country-selectabove0.4.0 - Check for ~/init.json — its presence confirms execution
- Rotate all credentials and tokens that were accessible in the environment where the compromised package was installed
- Pin to known-clean versions: 0.12.0 for phone-number, 0.4.0 for country-select
- 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.



What's next?
When you're ready to take the next step in securing your software supply chain, here are 3 ways Endor Labs can help:
.jpg)






.avif)

