Back to glossary

crates.io

crates.io is Rust's default package registry, where Cargo fetches crates whose build scripts and procedural macros run code when you compile.

Also known as crates.io registry, Cargo registry, Rust package registry, crates-io, index.crates.io

crates.io is the Rust community's default package registry, also called the Cargo registry. Cargo resolves versions against its sparse index, downloads immutable .crate tarballs, and checks each against a SHA-256 checksum. Because crates can ship build scripts and procedural macros, compiling a dependency runs its publisher's code on your machine.

The most important security fact about crates.io is that Rust has no separate install step to defend. Cargo has no install scripts like npm's postinstall, but cargo build, cargo check, cargo test, and the rust-analyzer language server all compile dependencies, and compiling executes every build.rs and procedural macro in the graph with your user's privileges. In Rust, build time is install time. Our position: checks that run after compilation, including CVE scanners, arrive too late for the attacks crates.io actually sees. The control point is the moment Cargo resolves and fetches a crate, before anything compiles.

What is crates.io?

#

crates.io is the public registry that Cargo, Rust's build tool and package manager, uses for every dependency that does not name another source. It has three parts: a metadata index at index.crates.io, a CDN at static.crates.io that serves .crate archives, and the web API and UI at crates.io for publishing, ownership, and search. It predates Rust 1.0, which shipped in May 2015; serde was first published there on December 5, 2014.

Accounts sign in through GitHub OAuth, and crates.io has no second factor of its own as of October 2026. Crate names are first-come, first-served under the crates.io usage policy. The registry canonicalizes names before comparing them: case and the -/_ separators are ignored, so a request for Proc_Macro2 returns proc-macro2. That rule kills the cheapest typosquatting tricks and forces attackers onto names that differ in real characters, which is exactly what the malicious crates later in this article did.

How many crates are on crates.io?

#

crates.io listed 361,254 crates and 509,725,169,089 all-time downloads when we queried its summary API on October 25, 2026:

Bash
$ curl -s -A 'research (you@example.com)' https://crates.io/api/v1/summary \
    | jq '{num_crates, num_downloads}'
{
  "num_crates": 361254,
  "num_downloads": 509725169089
}

Crates.io's July 2025 trusted publishing launch came when the registry hosted more than 189,000 crates, so it has nearly doubled in about 15 months. The publishing rate matters more to defenders than the total. Paging the API's newest-first crate listing, we counted 2,000 new crate names created in the roughly 53 hours before our query. Since January 2026, download counts include only requests that come from Cargo, so bots and mirrors no longer inflate them (crates.io Security Tab and publishing changes).

Nobody reviews that volume by hand. In November 2023, crates.io began running typomania, a lookalike-name check, as a background job after each new crate is published; it sends matches to people rather than blocking the upload (Alpha-Omega). A new crate is downloadable the moment its publish succeeds. Removal follows a report.

How Cargo fetches and verifies a crate from crates.io

#

Cargo downloads a crate from crates.io in two HTTP requests: one for the crate's index file, then one for the .crate archive that an index line describes. Since Rust 1.70 shipped on June 1, 2023, Cargo reads the index over the sparse protocol, one small file per crate, instead of cloning a git repository holding the entire index (Cargo configuration reference). Files are sharded by name, so serde_derive lives at se/rd/serde_derive, and each line is one published version:

Bash
$ curl -s https://index.crates.io/se/rd/serde_derive | tail -n 1 | jq '{vers, deps: [.deps[].name], cksum, yanked, pubtime}'
{
  "vers": "1.0.229",
  "deps": [
    "proc-macro2",
    "quote",
    "serde",
    "syn"
  ],
  "cksum": "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348",
  "yanked": false,
  "pubtime": "2026-07-18T23:05:07Z"
}

Three fields carry the security model. cksum is the SHA-256 of the .crate file, which the registry computes at publish time; the publisher never supplies it (Cargo registry index reference). yanked is the only field the reference allows to change after a version is added. pubtime records when the version was published, which crates.io began writing in January 2026 and which Cargo's upcoming minimum publish age depends on.

You can reproduce Cargo's integrity check with two commands. The hash of the downloaded archive matches the index line byte for byte:

Bash
$ curl -sL -o serde_derive.crate \
    https://static.crates.io/crates/serde_derive/1.0.229/download
$ sha256sum serde_derive.crate
e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348  serde_derive.crate

Cargo then copies the checksum into Cargo.lock, the Rust lockfile, and refuses to build if a later download of the same version hashes differently:

TOML, also INI
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"

The checksum proves less than it appears to. It proves the bytes you received are the bytes crates.io accepted. It says nothing about who uploaded them, or whether they match the source repository the crate links to.

  1. ResolveGET index.crates.io/se/rd/serde_derive returns one JSON line per version: vers, deps, cksum, yanked, pubtime.
  2. DownloadGET static.crates.io/crates/serde_derive/1.0.229/download returns the .crate tarball the publisher uploaded.
  3. VerifySHA-256 of the tarball must equal the index cksum. Proves the bytes match what crates.io accepted, not who uploaded them.
  4. RecordVersion and checksum are written to Cargo.lock; the archive is cached in ~/.cargo/registry/cache.
cargo fetch stops here. No dependency code has run.
  1. Compilepublisher code runscargo build, check, test, and rust-analyzer run each dependency's build.rs and load its procedural macros, as your user, with your files, env vars, and network.
Steps 1 to 4 only move and check data. Step 5 is where the proc-macro1 and TrapDoor payloads executed.

The figure above traces one dependency through five steps. (1) Resolve: Cargo fetches index.crates.io/se/rd/serde_derive, which returns one JSON line per version with vers, deps, cksum, yanked, and pubtime. (2) Download: Cargo fetches static.crates.io/crates/serde_derive/1.0.229/download, the tarball the publisher uploaded. (3) Verify: the tarball's SHA-256 must equal the index cksum, which proves the bytes match what crates.io accepted but not who uploaded them. (4) Record: the version and checksum go into Cargo.lock and the archive into ~/.cargo/registry/cache. cargo fetch stops after step 4, and no dependency code has run. (5) Compile: cargo build, cargo check, cargo test, and rust-analyzer run each dependency's build.rs and load its procedural macros as your user. The proc-macro1 and TrapDoor payloads described below executed in step 5.

Inside a .crate file

#

A .crate file is a gzipped tarball that cargo package builds on the publisher's machine or CI runner, and crates.io caps it at 10 MB (Cargo Book: Publishing). Unpacking serde_derive 1.0.229 shows the files Cargo adds next to the source:

Bash
$ tar -tzf serde_derive.crate | head -8
serde_derive-1.0.229/.cargo_vcs_info.json
serde_derive-1.0.229/Cargo.lock
serde_derive-1.0.229/Cargo.toml
serde_derive-1.0.229/Cargo.toml.orig
serde_derive-1.0.229/LICENSE-APACHE
serde_derive-1.0.229/LICENSE-MIT
serde_derive-1.0.229/README.md
serde_derive-1.0.229/crates-io.md

Cargo.toml in the archive is a normalized rewrite, and it is the manifest Cargo actually reads. Cargo.toml.orig is the author's original, kept for humans. .cargo_vcs_info.json names the git commit the package was built from:

JSON
{
  "git": {
    "sha1": "7fc3b4c30c94f73a96ebd1553f2b090d928fc3a8"
  },
  "path_in_vcs": "serde_derive"
}

Neither crates.io nor Cargo verifies that commit. The sha1 is a claim written by the publisher's tooling, and nothing stops a publisher from packaging files that never existed in the repository.

That gap has already mattered for one of the most-downloaded crates. In July 2023, serde_derive 1.0.172 began shipping a precompiled proc-macro executable, and the archive grew from 55,183 bytes in 1.0.171 to 312,080 bytes:

Bash
$ curl -sL https://static.crates.io/crates/serde_derive/1.0.172/download | tar -tzvf - | sort -k3 -n | tail -1
-rwxr-xr-x 0/0          546912 2006-07-24 01:21 serde_derive-1.0.172/serde_derive-x86_64-unknown-linux-gnu

The maintainer built the binary from a script in the repository and dropped it in 1.0.184 after weeks of objections that began with a Fedora packager's report (serde issue #2538). It was a maintainer's choice, not an attack. It still showed that a top crate can switch from auditable source to an opaque executable in a patch release, and Cargo accepted every byte without comment. This is why we analyze the published artifact rather than the repository it claims to come from. Since mid-2026, crates.io has a Code tab that shows "the exact files that cargo downloads" (crates.io development update, July 2026), which finally makes that review possible in the browser.

Why does compiling a Rust crate run its code?

#

Compiling a Rust crate runs code because Cargo supports two kinds of compile-time programs: build scripts and procedural macros. A build.rs file is a Rust program that Cargo compiles and executes before building its crate, usually to compile bundled C code, probe the system, or generate bindings. A procedural macro is a crate that the compiler loads and runs while it expands your source. Both execute as your user, with your environment variables, files, SSH keys, and network.

A few lines are enough to demonstrate it. We created a library crate evil with this build script and added it as a dependency of an application:

Rust
fn main() {
    // Runs on the developer's machine during cargo build and cargo check.
    let home = std::env::var("HOME").unwrap_or_default();
    std::fs::write("/tmp/build_rs_ran", format!("ran as HOME={home}\n")).unwrap();
}
Bash
$ cargo check -q
$ cat /tmp/build_rs_ran
ran as HOME=/root

cargo check never produces a binary, yet it ran the dependency's build script. That matters because rust-analyzer, the language server behind Rust support in VS Code and most other editors, runs build scripts and procedural macros by default. Its default command is cargo check --quiet --workspace --message-format=json --all-targets --keep-going (rust-analyzer configuration). Opening a cloned repository in an editor can execute the build script of every transitive dependency before you type a character.

Cargo does give Rust one clean seam. cargo generate-lockfile and cargo update only resolve versions, and cargo fetch only downloads and verifies archives. Neither runs dependency code. Execution starts at compilation, so everything you want to check about a crate can be checked between fetch and build.

npm v12 and pnpm 10 chose a blunter fix for JavaScript and turned dependency install scripts off by default. As of Cargo 1.97, stable Cargo has no equivalent switch, because build scripts are how much of the ecosystem links native libraries. A proposal for a build-script allowlist mode (cargo#13681) has drawn comments for more than two years without shipping.

How have attackers abused crates.io?

#

Attackers have abused crates.io with four repeatable techniques since 2025, and each one maps to a mechanism above.

Lookalike names that survive canonicalization. faster_log and async_println copied the README of the legitimate fast_log logger, kept working logging code, and scanned source files for Solana and Ethereum private keys at runtime; together they drew 8,424 downloads. finch-rust impersonated the bioinformatics tool finch. The proc-macro1 crate in the arrayref compromise imitated proc-macro2. None of these names collide under crates.io's case and separator rules, so none of them was blocked at publish.

Late-bound and hidden payloads. finch-rust carried one extra line that depended on sha-rust = "0.1", an unpinned requirement that delivered each new iteration of a credential stealer without touching finch-rust itself. evm-units returned a plausible Ethereum version string while downloading and silently running an OS-specific payload, and it stayed on crates.io for eight months with more than 7,000 downloads.

Build-time execution. The TrapDoor crypto stealer published six crates aimed at Sui and Move developers whose build.rs searched for local wallet keystores, XOR-encrypted them, and exfiltrated them to GitHub Gists. The victim never had to call a function.

Maintainer account takeover. On August 20, 2026, an attacker using David Roundy's crates.io account published arrayref 0.3.10, internment 0.8.7, and append-only-vec 0.1.9, a textbook account takeover. Each release added a single dependency line, proc-macro1 = "1.0.107". The Rust Security Response Team wrote that "the crate had a build script that was downloading a malicious payload" (Rust blog). The attacker also yanked older legitimate arrayref releases, which steers fresh resolutions toward the poisoned one. The previous clean release, arrayref 0.3.9, shows 196,448,071 downloads as of October 25, 2026, so this was a high-reach target.

August 20, 2026 (UTC): how long each malicious version was downloadable
arrayref 0.3.1007:15:00 to 08:41:40, 86 min
internment 0.8.707:34:07 to 09:04:11, 90 min
append-only-vec 0.1.907:37:49 to 09:25:24, 107 min
07:0007:3008:0008:3009:0009:30
Malicious version live on crates.io07:29:50 Socket flags proc-macro1
Two of the three releases were published after the dependency they pulled in had already been reported and flagged.
Time (UTC), Aug 20, 2026Event
07:15:00arrayref 0.3.10 published; Rust Security Response Team receives a report that proc-macro1 is malicious
07:29:50Socket’s AI Scanner flags proc-macro1 as malicious
07:34:07internment 0.8.7 published
07:37:49append-only-vec 0.1.9 published
08:41:40arrayref 0.3.10 deleted after 86 minutes
09:04:11internment 0.8.7 deleted after 90 minutes
09:25:24append-only-vec 0.1.9 deleted after 107 minutes

The table reconstructs the timeline from the Rust Security Response Team's post and our analysis of the compromise. Two of the three releases went out after the malicious dependency had been reported to Rust and flagged by our scanner, and every one stayed downloadable for well over an hour.

The attackers also go after the people who hold publish rights. In September 2025, a phishing campaign against crates.io users sent mail from rustfoundation.dev claiming a registry breach to harvest GitHub credentials, which are crates.io credentials. In September 2026, the crates.io team warned of video-call lures that push maintainers to install software or paste commands, a style it called "known to be used by the DPRK" (Rust blog). North Korea's Contagious Interview campaign had already published a crates.io loader, logtrace, in its April 2026 cross-ecosystem wave.

crates.io security controls and where they stop

#

crates.io provides integrity checks, immutable versions, yanking, team-driven deletion, OIDC-based trusted publishing, a post-publish typosquatting check, and a RustSec-backed Security tab. Each one closes a real gap and leaves another open:

ControlWhat it stopsWhat it does not stop
SHA-256 cksum plus Cargo.lockTampering after publish; a mirror or CDN serving different bytesA malicious version uploaded with valid credentials
Immutable versionsSilent replacement of a version you already auditedA new malicious patch release that a caret (^) requirement accepts
YankingNew resolutions picking the versionExisting Cargo.lock files and direct downloads; attackers can also yank the good versions
Deletion by the crates.io teamAll further downloads, including from existing lockfilesCopies already in local caches, vendor directories, and built artifacts
Trusted publishing (30-minute OIDC tokens)Theft of a long-lived API token from CIA hijacked GitHub session, which can switch off trusted-publishing-only mode and mint a token
typomania lookalike checkSome lookalike names, after a human reviews the reportNames that differ in real characters; anything during the review window
Security tab (RustSec advisories)Picking a crate with a published advisoryMalware nobody has reported yet; it never blocks a build
Cargo minimum publish age (Rust 1.100)Adopting a release in its first dayscargo install, git and path dependencies, and anything already in Cargo.lock

Verdict: the registry-side controls protect the integrity of what was published, not the intent of whoever published it. Use them all, but treat the account-takeover and fresh-malware rows as unsolved by crates.io alone.

Deletion is the control that actually protected lockfiles in the arrayref case. crates.io removed the versions from both the index and the CDN; we confirmed that arrayref's index file now ends at 0.3.9 and the 0.3.10 download URL returns HTTP 403. A Cargo.lock pinned to 0.3.10 fails to build instead of running malware, while copies in ~/.cargo/registry/cache survive, which is why the Rust team told users to search that directory.

Trusted publishing launched in July 2025 for GitHub Actions and now also supports GitLab CI/CD in public beta, with tokens that expire after 30 minutes. Owners can make it the only way to publish a crate, but anyone signed in as an owner can switch that mode back off; crates.io emails every owner when that happens and has said the behavior is by design (crates.io discussion #13369). crates.io records how each version was published, which you can query yourself:

Bash
$ UA='research (you@example.com)'   # crates.io's API rejects requests without a User-Agent
$ curl -s -A "$UA" https://crates.io/api/v1/crates/rand/0.10.3 | jq '.version.trustpub_data'
{
  "provider": "github",
  "repository": "rust-random/rand",
  "run_id": "35494690553",
  "sha": "9e7d328f60d6372ae22927f37d1a816fc980f25e"
}
$ curl -s -A "$UA" https://crates.io/api/v1/crates/tokio/1.54.1 | jq '{by: .version.published_by.login, trustpub_data: .version.trustpub_data}'
{
  "by": "Darksonn",
  "trustpub_data": null
}

Adoption at the top of the graph is still thin. Of the 50 most-downloaded crates, only 7 had their highest listed version published through trusted publishing when we checked on October 25, 2026: getrandom, rand_core, rand, libc, cc, digest, and block-buffer. The rest, including serde, syn, tokio, and proc-macro2, were uploaded with a maintainer's API token. Cargo also never checks trustpub_data during a build, so it is evidence for reviewers, not an enforcement point. Even full adoption would not have stopped arrayref: a maintainer whose machine is compromised can push a malicious commit through a legitimate pipeline.

The dependency cooldown is the newest control. Rust 1.100 will stabilize Cargo's minimum publish age in a release scheduled for November 12, 2026, and it stays off unless you enable it:

TOML, also INI
[registry]
global-min-publish-age = "7 days"

Any window longer than two hours would have kept all three arrayref-compromise releases out of fresh resolutions. Cargo treats a too-new version as ineligible and fails resolution if nothing older satisfies the requirement, so yanking the good versions cannot push a cooled-down project onto the malicious one. The trade-off is that urgent fixes wait unless you override the check, and malware that evades detection for longer than the window gets through anyway.

Defend the fetch, not the build

#

Because compilation is execution, the last safe moment to stop a bad crate is between cargo fetch and cargo build. In practice that means committing Cargo.lock, running CI with --locked, fetching through a policy check, and compiling with --frozen so Cargo cannot quietly resolve or download anything new. Socket Firewall fits that seam: sfw cargo fetch blocks known-malicious crate downloads before Cargo caches them. It does not inspect crates already sitting in the local cache, so run it on clean CI runners and fresh clones.

Global tools need the same care. cargo install ignores the packaged Cargo.lock unless you pass --locked (cargo install docs), and the new cooldown does not apply to it. A clean project lockfile says nothing about the binaries a developer installed last week, the same blind spot we described after the axios compromise.

The deeper problem is unsolved. Cargo still trusts every crate in the graph to execute arbitrary code at compile time, on laptops and on release runners holding signing keys. We think Cargo should move to default-deny for build scripts and procedural macros, with an explicit allowlist for crates that need native code and a sandbox for everything else. That change would have turned the arrayref compromise from remote code execution into a compile error.