A crates.io account takeover doesn't require a zero-day exploit. In the Rust ecosystem, the fastest route from a phished GitHub credential to a full supply chain compromise runs straight through the crates.io publish pipeline: hijack the account of a maintainer whose crate thousands of projects depend on, push a new version laced with a build.rs payload or a malicious dependency, and let cargo build do the rest. This case study walks through how that attack chain works in practice, grounds it in the documented rustdecimal malware incident and the broader pattern of registry account takeovers seen across npm and other package ecosystems, and lays out concrete steps for maintainers and consumers of the Rust supply chain.
It's worth being precise about terminology up front, because these incidents get conflated. crates.io has not, to date, had a single high-profile disclosure of a popular, pre-existing crate whose maintainer account was hijacked and used to ship a backdoored point release — the kind of thing that happened to event-stream on npm in 2018 or ua-parser-js in 2021. What crates.io has seen is a documented malicious-crate incident (the rustdecimal typosquat), plus a registry authentication and token model that is structurally identical to the ones that made those npm takeovers possible. This piece treats crates.io account takeover as what it is: a live, plausible, and partially-realized risk that the crates.io team has been actively hardening against, not a single closed incident with a tidy postmortem.
What a Crates.io Account Takeover Looks Like
crates.io authentication is delegated entirely to GitHub OAuth — there is no separate username-and-password login. That design choice is generally good for security (no password database to breach), but it also means a crates.io account takeover is, in practice, a GitHub account takeover: phish or credential-stuff a maintainer's GitHub login, steal a session cookie, or compromise a device with an active GitHub session, and you inherit whatever crates.io publish rights are attached to that identity. Combine that with the historical crates.io API token model — tokens that, before scoped tokens were introduced, could publish or yank any crate the account owned, with no expiration — and a single leaked token (committed to a public repo, exposed in a CI log, or exfiltrated from a compromised developer machine) was equivalent to full account compromise for supply chain purposes.
Once an attacker holds publish rights, the payload delivery mechanism in the Rust ecosystem is unusually potent. Unlike many package managers, Cargo will execute a crate's build.rs script as native code on the building machine before compilation even starts, with no sandboxing by default. A malicious crate version doesn't need to wait for a vulnerable code path to be called in production; it can download a second-stage payload and execute it the moment a developer or CI runner runs cargo build.
Affected Versions and Components
There's no single vulnerable version string here, because this isn't a bug in Cargo or crates.io's code — it's a trust and identity weakness in the publishing pipeline. The components in scope are:
- crates.io registry: the publish/yank API and the ownership model that determines who can push new versions of a crate.
- GitHub OAuth as the sole identity provider for crates.io accounts, meaning GitHub-side account security (2FA, session hygiene) is inherited directly by the Rust registry.
- Cargo API tokens: historically long-lived and, prior to the scoped-tokens rollout, unscoped across all owned crates.
cargo build/build.rsexecution: the mechanism that turns a malicious crate version into arbitrary code execution on developer and CI machines.
The concrete, documented case that illustrates the "malicious crate version" half of this story is rustdecimal, a crate uploaded to crates.io in 2022 that typosquatted the popular rust_decimal library. It shipped a build script that fetched and executed a downloader payload targeting Windows and Linux hosts. It was not the product of an account takeover — it was a new, unrelated package riding on a familiar name — but it demonstrated, in production, that crates.io's publish and review pipeline would accept and distribute a crate carrying live malware, and that build.rs execution is exactly the vector an account-takeover attacker could also use against an already-trusted crate.
CVSS, EPSS, and KEV Context
This is the section where it's tempting to bolt on a severity score, and where accuracy matters most. Malicious-package and account-takeover incidents in package registries are generally not assigned a traditional CVE with an NVD-published CVSS vector, because they aren't a flaw in a specific piece of software at a specific version — they're an abuse of a trust relationship. rustdecimal and comparable malicious Rust crates are tracked through the RustSec Advisory Database and, where cross-listed, GitHub Security Advisories (GHSA), rather than through CVE/NVD scoring. Correspondingly, there is no EPSS probability-of-exploitation score to cite here (EPSS scores CVEs) and no CISA Known Exploited Vulnerabilities (KEV) entry, since KEV is also CVE-scoped.
That absence of a score is not the same as low severity. The relevant weakness classes are well established even without a CVSS number: CWE-506 (Embedded Malicious Code), CWE-829 (Inclusion of Functionality from Untrusted Control Sphere), and CWE-287 (Improper Authentication) for the account-takeover vector itself. In practical terms, a successful crates.io account takeover that ships a malicious version of a widely depended-on crate has a blast radius comparable to a critical, wormable CVE: every downstream project that runs cargo build or cargo install against the new version is a potential victim, with no exploitation step beyond "the build ran." Treat this class of risk as CVSS-critical-equivalent when prioritizing defenses, even though no registry entry will hand you that exact number.
Timeline
- 2018 — The
event-streamnpm incident: a maintainer transferred publish rights to an unknown contributor, who added a dependency that stole cryptocurrency wallet credentials from downstream applications. This became the reference case for "trusted maintainer handoff" as an account-takeover-adjacent risk across all registries, Cargo included. - October 2021 — Multiple versions of
ua-parser-json npm were published from a compromised maintainer account, containing cryptomining and credential-stealing code, reinforcing that this pattern was not a one-off. - 2022 — The
rustdecimalmalicious crate is published to crates.io and identified as a typosquat ofrust_decimal, containing abuild.rs-driven downloader targeting Windows and Linux. It stands as one of the clearest documented rust supply chain incident examples of malware distribution through the Cargo ecosystem, tracked in the RustSec Advisory Database. - 2023 — crates.io ships scoped API tokens, letting maintainers issue tokens limited to specific crates and specific actions (publish vs. yank vs. read) instead of a single all-powerful credential — a direct response to the token-based account-takeover exposure described above.
- June 2024 — The polyfill.io incident: a domain and CDN service trusted by hundreds of thousands of sites changed ownership and began serving malicious JavaScript. Not a crates.io security incident, but a stark illustration of how a single compromised distribution point ripples through dependents that never re-review what they're pulling in.
- Ongoing — Registries across ecosystems, including crates.io, have been moving toward trusted publishing (short-lived, OIDC-issued credentials tied to a specific CI job rather than long-lived personal tokens), following the model PyPI pioneered, specifically to remove standing tokens as an account-takeover target.
Remediation Steps
For crate maintainers:
- Lock down the GitHub account behind your crates.io identity. Since crates.io login is GitHub OAuth only, GitHub account security is crates.io account security. Enable hardware-key or app-based 2FA, and audit active sessions and OAuth app grants regularly.
- Move to scoped, short-lived tokens. Replace old unscoped crates.io tokens with scoped tokens limited to the specific crate and action needed, and rotate them on a schedule. Adopt trusted publishing from CI where it's available so no long-lived secret sits in a repo or pipeline at all.
- Never let tokens land in logs, env dumps, or public repos. Treat a crates.io token with the same handling discipline as a cloud provider credential.
- Review your crate's owner list. Remove former collaborators and unused CI-service accounts that retain publish rights.
- Watch your own publish activity. crates.io emails owners on new version publishes; treat an unexpected notification as an incident, not spam.
For consumers of crates:
- Pin with
Cargo.lockand commit it, including for libraries where it's sometimes skipped — a floating dependency range is an open door to whatever version gets published next, malicious or not. - Run
cargo auditandcargo denyin CI against the RustSec Advisory Database to catch known-malicious and known-vulnerable crate versions before they reach a build. - Diff dependency updates, especially changes to
build.rs. A version bump that suddenly adds network calls, shell-outs, or binary downloads at build time is the single strongest signal of a compromised crate. - Restrict build-time network egress in CI where feasible, so a malicious
build.rshas nowhere to phone home. - Track provenance, not just version numbers, by generating and maintaining an SBOM so you know precisely which crate versions, and which maintainers, sit in your dependency tree at any point in time.
How Safeguard Helps
Safeguard is built for exactly this gap: the moment a dependency you already trusted changes underneath you. For teams building on crates.io and the broader Rust supply chain, Safeguard continuously monitors your dependency graph for the signals that distinguish a routine crate update from a crates.io security incident in progress — new or first-time maintainers publishing a version, build.rs scripts that suddenly add network or shell behavior, version bumps that don't match the upstream repository's tags or commit history, and packages that resemble popular crates closely enough to be typosquats.
Safeguard generates and maintains SBOMs across your Rust (and polyglot) codebases automatically, so when a malicious crate version or a compromised-maintainer publish surfaces, you can answer "are we exposed, and where" in minutes instead of days of manual Cargo.lock archaeology. Policy controls let you gate CI on dependency risk score, block unreviewed major-version or maintainer-ownership changes from auto-merging, and enforce lockfile integrity across every repository, not just the ones someone remembered to configure. And because this class of incident rarely arrives with a convenient CVE or CVSS score attached, Safeguard's detections are built around behavioral and provenance signals — not just CVE feed matching — so a rust supply chain incident like a crates.io account takeover gets flagged from the anomalous publish itself, not weeks later when someone finally writes the advisory.