Rust Supply Chain Attack Analysis and the build.rs Problem

php2shell patches (CVE-2026-109618 and CVE-2026-109619) for EOL PHP are out now. With sales replies slower than usual due to high demand, the fastest route to patched packages is to buy ELS for Runtimes online (opens in new tab). Read the patch FAQ

Go straight to an AI summary of this blog post instead

  • Summarize this post with Perplexity
  • Summarize this post with Gemini

Key Takeaways

  • A malicious build.rs script runs during cargo build with the privileges of whatever process invoked Cargo.
  • Stable Cargo gives you no supported way to compile a dependency tree while refusing to run its build scripts.
  • Six of TrapDoor’s 34 malicious packages landed on crates.io, against 21 on npm and 7 on PyPI.
  • npm and pnpm now block unapproved dependency install scripts by default, and stable Cargo has no dependency-wide equivalent.

Rust developers have spent years hearing that the language is safe. Memory safety, no data races in safe code, a type system that catches at compile time what other languages only trip over at runtime. None of that touches code that runs before your program exists. In May 2026, a software supply chain campaign named TrapDoor published 34 malicious packages across more than 384 versions on npm, PyPI, and crates.io. Not one of them waited for you to call the package. They fired at install, on import, or during compilation. Six of the 34 were on crates.io, dressed up as Sui and Move tooling, using build.rs to hunt for crypto wallet keystores. A crate author can switch off their own build script in the manifest. As a consumer, you have no general-purpose switch that disables dependency build scripts across the tree. That’s the gap this article is about.

What TrapDoor Did Across Three Registries

npm, PyPI, and crates.io weren’t hit equally. Of the 34 packages identified, 21 were on npm, using postinstall scripts to run a shared credential-harvesting payload the moment someone installed one. Seven were on PyPI, running on import to fetch a remote JavaScript payload and execute it locally. Only six landed on crates.io, using Rust build scripts that execute during compilation. The pattern is what ties the campaign together. In every ecosystem, the attacker reached for the execution hook that ecosystem already handed them.

Registry Named packages Execution hook What it did
npm 21 Postinstall lifecycle scripts Ran trap-core.js, a shared credential harvester with SSH-based lateral movement
PyPI 7 Import-time execution Fetched and ran a remote JS payload via node -e
crates.io 6 build.rs build scripts Searched for Sui and Aptos wallet keystores, XOR-encrypted them, and exfiltrated to GitHub Gists

The six crates.io packages – move-analyzer-build, move-compiler-tools, move-project-builder, sui-framework-helpers, sui-move-build-helper, and sui-sdk-build-utils – were aimed narrowly at Sui and Move developers rather than the general Rust population. The npm side carried most of the damage in package count and payload sophistication. The crates.io side still matters most to Rust engineers, because it shows a build.rs script doing exactly what a build.rs script is positioned to do. It runs quietly, with the privileges of the build process, before the application itself ever gets a chance to run.

Why build.rs Is a Better Target Than a Postinstall Script

Execution hooks across npm, PyPI and crates.io in the TrapDoor Rust supply chain attack
Both npm and pnpm now block unapproved dependency install scripts by default, with an allowlist you approve and commit for the packages that genuinely need them. pnpm has done it since v10 in January 2025. npm only got there with v12 in July 2026, weeks after TrapDoor, so those postinstall payloads still ran by default for anyone installing during the campaign. Stable Cargo has no equivalent dependency-wide allowlist or default block. build.rs is a Rust program Cargo compiles and runs before compiling the crate it belongs to, with the privileges of the process running Cargo. It exists for good reasons, like code generation and native linking, and plenty of legitimate crates ship one. Any dependency whose build script runs can execute arbitrary code with the permissions available to the Cargo process. TrapDoor is what that looks like when somebody uses it deliberately. The six crates named to look like Sui and Move build tooling used build.rs to search a machine for wallet keystores the moment someone ran cargo build. The script XOR-encrypted whatever it found and shipped it to GitHub Gists. No separate install hook, no import, just compiling a project that happened to depend on one of six package names. Procedural macros are the other compile-time execution path in Rust, and TrapDoor didn’t need them. A proc macro runs inside the compiler during macro expansion, which means it only fires when something actually invokes it. A build script fires because the crate sits in the build graph at all, which is exactly what makes it the more dependable option for an attacker.

Mechanism Runs during General package-manager control
npm lifecycle script Install Yes, blocked by default since npm 12
Python import-time execution Package import No
Rust build.rs Compilation No
Rust procedural macro Macro expansion No

Cargo offers no stable, general-purpose switch that lets a consumer compile a dependency tree while refusing dependency build scripts or macro expansion. There is one narrow exception. When a crate declares a links key, you can override its build script from Cargo configuration by supplying the metadata yourself, and that script then never gets compiled or run. It covers native library shims, not an arbitrary dependency that decided to ship a build script. cargo build --offline prevents Cargo from making network requests and a committed Cargo.lock pins versions, but neither stops a build script already sitting in your tree from running, and neither helps if the version your lockfile pins is the malicious one.

Where a Build Script Runs and What It Can Reach

Credentials a malicious build.rs can reach on a CI runner versus a developer laptop
A CI runner doing cargo build typically holds cloud role credentials, registry tokens, container registry access, and sometimes signing keys. A developer laptop can hold most of that plus SSH keys, browser session data, and source for whatever hasn’t shipped yet. That’s what makes a build host worth compromising in the first place. Sakshi Grover, senior research manager for IDC Asia Pacific Cybersecurity Services, told CSO Online after TrapDoor surfaced that “a single compromised workstation can quietly become an entry point into CI/CD pipelines.” A build script harvesting credentials is one way in, and pulling wallet keystores straight out of the filesystem during an unwatched build is what the crates.io side of TrapDoor did.

The Part Aimed at AI Coding Assistants

How TrapDoor poisoned CLAUDE.md and .cursorrules files read by AI coding assistants
TrapDoor’s poisoning of AI assistant files wasn’t a build.rs trick. It came from the npm payload planting .cursorrules and CLAUDE.md files as persistence, with the malicious instructions hidden behind zero-width Unicode, plus the same attacker opening GitHub pull requests against unrelated AI and developer tooling projects. Both filenames are conventions AI coding tools read to understand a codebase, so a poisoned one is indirect prompt injection. Attacker text lands in a file the assistant treats as guidance instead of data. The precondition is narrower than “every AI tool is compromised,” though. Whether the technique lands depends on which instruction files a given assistant discovers, trusts, and acts on. Worth doing anyway. Search for assistant instruction files anywhere other than your repository root, and treat a hit inside a vendored or path dependency as suspicious by default. It works as a CI check, as long as you invert the exit code so that finding something actually fails the build rather than passing it.

if find . -name 'CLAUDE.md' -o -name '.cursorrules' \
  | grep -vE '^\./(CLAUDE\.md|\.cursorrules)$' | grep -q .; then
  echo "Unexpected AI assistant instruction file found"
  exit 1
fi

That’s a different goal than tricking a human reviewer. Poisoning a file at persistence time means the payoff comes later, whenever an assistant next reads the project and treats the file as legitimate instruction. The fix is mostly about scope. Narrowing which instruction files an assistant trusts shrinks the surface, particularly when dependency-controlled paths are excluded.

What Yanking a Crate Actually Removes

The crates.io registry does something unusual, and mostly good. Published versions are immutable and can’t be deleted, only yanked. That blocks a whole class of attack that mutable registries suffer from. It also means a yank is weaker remediation than most people assume.

Action Effect
Yank a version New resolutions skip it, existing lockfiles keep using it
Yank all versions The crate name stays taken, the code stays downloadable
Delete a version Not possible on crates.io

A yanked version already recorded in a committed Cargo.lock stays in use. That’s deliberate, so builds don’t break on their own, but it means an organization that pinned a malicious version keeps building with it until somebody actually touches the lockfile. Run cargo update --dry-run to see what would change, then run the matching cargo update to rewrite the lockfile once you’ve confirmed the replacement versions are safe.

Why This Was Always Going to Happen to Rust

Attackers follow adoption. Rust’s expanding role across infrastructure, cryptography, and systems software over the past several years made this a matter of when, not if. Crates that were small side projects a few years ago now sit deep inside production stacks. Build scripts are culturally normal in Rust in a way postinstall scripts aren’t in npm. A package with a postinstall script draws a second look from reviewers. build.rs doesn’t, because crates that generate code, probe platform capabilities, or link native libraries all have a legitimate reason to ship one. That legitimate use is what makes the mere presence of build.rs such a weak signal. None of this makes TrapDoor crates.io’s first brush with malware. Back in 2023 the registry pulled nine typosquatted crates whose build.rs files shipped host metadata off to a Telegram channel, and supply chain attacks on crates.io have landed often enough since that the team stopped blogging about every one in February 2026. What’s new with TrapDoor is crates.io turning up inside a coordinated campaign that hit npm and PyPI in the same wave. Those two went through their own version of this years earlier, on a much larger scale, once their package counts made typosquatting and lookalike names worth an attacker’s time. TrapDoor puts crates.io inside the same cross-ecosystem threat model security teams already apply to npm and PyPI.

Securing the Rust Supply Chain

SecureChain for Rust screens a crate before it enters your dependency tree, reviewing source for build-time attack patterns, including malicious build.rs scripts and procedural macros, and blocking the ones that fail. Crates get rebuilt from verified source rather than passed straight through from the public registry. That’s the kind of gate that has to sit in front of a package like sui-framework-helpers, because once a hostile build.rs is in the tree, running cargo build is already too late. It also keeps patching vulnerabilities in crates already sitting in your stack, including the ones whose maintainers have gone quiet. TrapDoor is the immediate problem. Most of what ends up in a production Rust tree is the slower one, and it’s worth covering too. Subscribe to the TuxCare newsletter for analysis like this the next time a campaign picks a new package name and a new ecosystem to hide in.

FAQ

What is a Rust supply chain attack?

An attack that compromises code or dependencies before they reach you, most often through malicious or hijacked crates on crates.io. In Rust, build scripts and procedural macros are the two compile-time execution paths that matter most, because both run code before the resulting application does.

What is build.rs and why is it dangerous?

build.rs is a Rust program Cargo compiles and runs before building the crate it belongs to, with the privileges of the process running Cargo. It exists for legitimate code generation and native linking, and any dependency that ships one can execute arbitrary code with whatever permissions the Cargo build process has.

Did TrapDoor actually need build.rs, or would any Rust build hook have worked?

build.rs was enough, and it was the least effort, so that’s what the six crates used. A procedural macro is the other compile-time route, but it only runs when the consuming code actually invokes the macro, whereas a build script runs simply because the crate is in the build graph. The attackers picked build.rs because it gave them the execution point they needed.

Does yanking a crate remove it from crates.io?

No. Yanking stops new resolutions from selecting the version while leaving it downloadable, and a version already pinned in an existing Cargo.lock keeps getting used. Deleting a published version isn’t possible on crates.io.

What were the poisoned AI assistant files in TrapDoor?

The .cursorrules and CLAUDE.md files used zero-width Unicode characters to conceal instructions aimed at AI coding assistants. The campaign also tried to introduce similar files through GitHub pull requests on unrelated projects.

Summary
Rust Supply Chain Attack Analysis and the build.rs Problem
Article Name
Rust Supply Chain Attack Analysis and the build.rs Problem
Description
TrapDoor hid malicious build.rs scripts in six crates that stole wallet keys during compilation, and Cargo still can’t block them.
Author
Publisher Name
TuxCare
Publisher Logo

Join 4,500+ Linux & Open Source Professionals!

2x a month. No spam.

Table of Contents
Get the open-source security answers you need

Join 4,500+ Linux & Open Source Professionals!

2x a month. No spam.