TrapDoor: Rust build.rs, npm postinstall & PyPI imports steal crypto dev keys
Contents
TL;DR
What happened 34 packages across npm (21), PyPI (7) and Crates.io (6), tracked by Socket as “TrapDoor.” They steal Solana/Aptos/Sui/Move dev keys plus SSH keys, GitHub/AWS credentials, and browser wallet data.
Where it fires npm postinstall, PyPI at import time, Crates.io build.rs. Adding the package and running install/build is enough — you never have to call its functions.
What to do If any machine pulled these in:
- Treat the machine as compromised and move wallet assets to a fresh environment
- Rotate SSH keys, GitHub tokens and AWS keys in order
- Remove old public keys from GitHub, server
authorized_keys, and CI deploy keys
TrapDoor, the supply chain attack Socket has been tracking, is an unusual cross-registry crypto stealer: 34 packages spread across npm, PyPI and Crates.io.
What stood out isn’t the part that poisons AI config files — we’ve seen that in earlier attacks. The core of this one is that it uses Rust’s build.rs as a trigger to go straight for Sui/Move keystores, and that each registry has its own trigger timing and its own encryption scheme.
The short DEV Community write-up framed it as “for Solana developers,” but Socket’s own report has a wider focus.
Sui, Aptos, Move, DeFi, Solidity, and even small tools disguised as build helpers — the names are planted ahead of time to match what blockchain developers search for mid-task.
Each registry fires differently — and encrypts differently
The most distinctive thing about TrapDoor is that the timing of when code runs and the encryption of the stolen data are designed separately per registry.
Rather than reusing one payload, it changes the trigger to fit each registry’s behavior.
| Registry | Count | Trigger | Encryption | Exfil to |
|---|---|---|---|---|
| npm | 21 | postinstall | Fernet (symmetric) + ECDH key exchange | attacker infra |
| PyPI | 7 | remote JS run at import | none (plaintext) | webhook |
| Crates.io | 6 | build.rs (build time) | XOR (key cargo-build-helper-2026) | GitHub Gist |
On npm it adds Fernet symmetric encryption plus an ECDH key exchange to protect the channel, while PyPI just sends plaintext to a webhook and Crates.io uses a crude fixed XOR key — three wildly different levels of polish.
The same attacker is applying a different implementation per registry. npm’s trap-core.js is a worked-out 48,485 bytes / 1,149 lines, whereas the PyPI side is thin: at import it pulls down external JS and runs it with node -e.
flowchart TD
A["Add dep / build"] --> B{"Registry"}
B --> C["npm<br/>postinstall"]
B --> D["PyPI<br/>at import"]
B --> E["Crates.io<br/>build.rs"]
C --> F["trap-core.js<br/>Fernet+ECDH"]
D --> G["external JS via node -e<br/>plaintext webhook"]
E --> H["scan keystores<br/>XOR to Gist"]
F --> I["SSH keys / GitHub / AWS / wallets"]
G --> I
H --> I
Rust’s build.rs is the new trigger to watch
npm’s postinstall is a fairly well-known path, but this is the first time this blog has covered a supply chain attack that abuses Crates.io’s build.rs.
That’s where the novelty sits.
build.rs is a Rust crate’s build script, originally meant for linking C libraries and code generation.
The problem is the timing: the moment you add that crate as a dependency and run cargo build, arbitrary code runs before you ever call a single library API.
On the npm side there’s at least some way to stop install-time scripts, like --ignore-scripts or pnpm 11’s minimumReleaseAge (covered in the post on pnpm 11 making minimumReleaseAge the default).
Cargo has no standard mechanism to disable build.rs across the board, so you’re left avoiding it on the assumption that it runs at build time.
TrapDoor’s six Crates.io packages all carry names aimed at Sui/Move developers.
sui-framework-helperssui-move-build-helpersui-sdk-build-utilsmove-analyzer-buildmove-compiler-toolsmove-project-builder
They all look like “Sui SDK build helpers” or “Move compiler tooling,” and inside build.rs they scan for local keystores, encrypt the keys they find with the fixed XOR key cargo-build-helper-2026, and push them out as a public GitHub Gist file.
Using a public Gist as the exit is the clever part: the attacker doesn’t need their own server, and the data leaves over ordinary HTTPS traffic to GitHub.
The target is blockchain developers’ keys
What TrapDoor reaches for, on top of the usual developer secrets (SSH keys, GitHub tokens, AWS credentials), is the keys sitting on a blockchain developer’s machine.
Solana, Aptos, Sui, and browser wallet extensions like MetaMask and Phantom are all in scope.
A blockchain developer’s machine is efficient from an attacker’s point of view.
The private keys tied directly to assets and the dev keys used for deploy and testing are often kept on the same machine. Socket doesn’t publish exactly which paths TrapDoor reads, but each chain’s CLI puts keys in roughly fixed default locations.
| Chain | Default key location (reference) |
|---|---|
| Solana | ~/.config/solana/id.json |
| Sui | ~/.sui/sui_config/sui.keystore |
| Aptos | ~/.aptos/config.yaml (private key inside the profile) |
Wallet extension keys and seed phrases live under the browser profile.
Packages calling themselves “backup checkers” or “safety checks,” like mnemonic-safety-check or wallet-backup-verifier, are placed exactly where they can touch that seed phrase — posing as a “tool to check whether your private key leaked,” then carrying off the private key while it’s at it.
Poisoning AI config files is an extension of known tactics
npm’s trap-core.js validates the stolen AWS/GitHub credentials against their respective APIs before using them, persists through Git hooks, shell hooks, systemd and cron, and goes as far as SSH-based lateral movement (moving to other hosts with stolen keys).
Part of this is writing into .cursorrules and CLAUDE.md, planting instructions hidden with zero-width Unicode characters.
But the tactic itself isn’t new.
Targeting AI dev environments showed up in SANDWORM_MODE and Mini Shai-Hulud leaving traces in Claude Code/VS Code; hiding with zero-width Unicode showed up in StegaBin; the chain that poisons the next AI session through config files showed up in Clinejection.
The closest read is that TrapDoor reuses these known parts inside a crypto stealer. Its originality is not the AI poisoning — it’s the Crates.io and blockchain-key side.
The dangerous line isn’t “did you call it” but “did you pull it in”
Whether your app actually called the package’s functions is irrelevant.
npm has postinstall, Crates.io has build.rs, PyPI runs at import — once you add it as a dependency and an install or build runs, there’s a path to the secrets on your machine. Trying it once in a throwaway test repo, or just having it run in CI, counts the same.
npm audit, pip audit and cargo audit are mainly tools for looking at known-vulnerability databases; they don’t directly stop the behavior of a freshly published malicious package, a postinstall, a build script, or a remotely fetched payload.
What to check, and the IOCs
Treat any machine that may have pulled these in as compromised, rather than stopping at uninstalling.
Socket’s IOCs are usable for log searches.
| Type | Value |
|---|---|
| GitHub Pages | ddjidd564[.]github[.]io, ddjidd564[.]github[.]io/defi-security-best-practices/ |
| Accounts | GitHub ddjidd564 / npm asdxzxc / PyPI asdmini67, dae5411 |
| Payload | trap-core.js (48,485 bytes), campaign ID P-2024-001 |
| XOR key | cargo-build-helper-2026 |
Slice the places to check by machine and surrounding services, not by registry.
| Where | What to look at |
|---|---|
| Dev machine | ~/.ssh, each chain’s keystore, browser extensions, .env, shell config, cron, systemd |
| Git repos | .git/hooks/, CLAUDE.md, .cursorrules, unfamiliar config files |
| GitHub | PATs, OAuth apps, Actions secrets, recent Gist creation / PRs / commits |
| Cloud | access-key usage history, IAM permissions, unusual API calls |
| CI | recent npm/pip/cargo install and build logs, dependency additions, secret usage |
Wallets are a separate case.
If keys or seeds for Solana, Aptos, Sui, MetaMask or Phantom were sitting on the machine, move the assets to a fresh environment on the assumption they’ll be drained later. Nothing shows on-chain at the moment the private key is taken; you only notice once funds move.
Rotate SSH keys, GitHub tokens and AWS keys in order.
After swapping an SSH key, remove the old public key from GitHub, the server’s authorized_keys, and CI deploy keys. Deleting a GitHub token alone leaves the path to repos and servers open if a stolen SSH key remains.
It targets a gap that cooldowns can’t close
Socket says it confirmed the first PyPI package, eth-security-auditor@0.1.0, at 2026-05-22 20:20:18 UTC, and detected 381 package-version records at a mean of 5m56s, median 5m27s, fastest 58s.
With detection that fast, plenty of environments could have avoided this just by letting new releases sit for minutes to hours — a cooldown.
The catch is that many of these names look less like permanent dependencies and more like “a small tool you search for once mid-task and install.”
Names like wallet-security-checker or move-compiler-tools are harder to stop with a cooldown than dependencies pinned in a lockfile. Instead of the few-minute window between publish and detection, it goes after the moment a developer searches and installs on the spot.