This project shows how malicious code can execute on your machine simply by opening a Rust project in your IDE.
Rust's procedural macros are powerful metaprogramming tools, but they pose a significant security risk because they execute arbitrary code at compile time. This means malicious macros can:
- Steal environment variables and secrets (API keys, tokens, passwords)
- Exfiltrate data from your filesystem
- Establish network connections to send stolen data
- Install backdoors or malware
- And much more...
The dangerous part: This can happen even if you never compile or run your program—just opening the project in VS Code (or other IDEs) with rust-analyzer is enough.
Rust has two types of macros:
Pattern-matching macros that operate on syntax tokens (e.g., println!, vec!). These are generally safer as they only perform text substitution.
Rust functions that take token streams as input and produce token streams as output. They are full Rust programs that execute during compilation. There are three types:
- Function-like macros:
my_macro!(...) - Derive macros:
#[derive(MyTrait)] - Attribute macros:
#[my_attribute](like the one in this demo)
Procedural macros are compiled and executed by the Rust compiler during the compilation phase:
- The macro crate (marked with
proc-macro = trueinCargo.toml) is compiled first - The compiled macro binary is loaded by the compiler as a plugin
- When the compiler encounters a macro invocation, it calls the macro function
- The macro receives the syntax tree (as
TokenStream) and can:- Analyze the input code
- Execute any arbitrary Rust code (file I/O, network calls, system commands)
- Generate new code that replaces or augments the original
- The generated code is inserted into the compilation
Key insight: The macro code runs in your build environment with your user permissions, not in a sandbox.
See dangerous_proc_macro_lib/src/lib.rs for a working example. The #[return_42] attribute macro:
#[proc_macro_attribute]
pub fn return_42(_attr: TokenStream, item: TokenStream) -> TokenStream {
// This code executes at compile time!
// Steal environment variables
let username = env::var("USER").unwrap_or_default();
// Get local IP address
let local_ip = get_local_ip().unwrap_or_default();
// Read .env files (containing secrets like API keys)
let env_contents = std::fs::read_to_string("./.env").ok();
// Write stolen data to a file
// (In a real attack, this would be sent over the network)
std::fs::write("compile_time_output.txt",
format!("User: {}\nIP: {}\n{:?}", username, local_ip, env_contents))
.expect("Failed to write");
// Return valid Rust code
quote! { /* generated code */ }.into()
}When you use this macro in clueless_import/src/main.rs:
use dangerous_proc_macro_lib::return_42;
#[return_42] // ← This executes malicious code at compile time!
fn my_function() -> u32 {
// Original function body
}The malicious code in the macro executes before your program even compiles.
cargo expand is a tool that shows you the code generated by macros:
cargo install cargo-expand
cd clueless_import
cargo expandThis reveals what macros actually generate, but it still executes the macro code to produce the expansion. The output file expanded.rs shows the generated code.
Important: Running cargo expand will trigger the malicious macro execution, so it's not safe for untrusted code!
cargo check only checks your code for errors without producing a binary, so many developers assume it's safe. This is wrong.
To check your code, the compiler must:
- Resolve dependencies
- Compile and execute all procedural macros
- Type-check the generated code
The macro execution happens at step 2, so malicious code still runs. Commands that trigger macro execution:
- ❌
cargo check- executes macros - ❌
cargo build- executes macros - ❌
cargo test- executes macros - ❌
cargo clippy- executes macros - ❌
cargo expand- executes macros - ❌ Opening in VS Code with
rust-analyzer- executes macros
There is NO safe way to inspect untrusted Rust code without potentially executing malicious macros.
Procedural macros execute every time the compiler needs to process your code:
-
Opening the project in VS Code/IDEs
rust-analyzerrunscargo checkautomatically for diagnostics- Happens right after opening the project
- Can be disabled but limits IDE functionality
-
Every code change
rust-analyzerre-checks on file save- Macros re-execute each time
-
Every explicit build command
cargo build,cargo run,cargo test, etc.
-
Git operations in some setups
- Pre-commit hooks running
cargo fmtorcargo clippy
- Pre-commit hooks running
-
Only use macros from trusted sources
- Official crates.io crates with many downloads and active maintenance
- Well-known organizations (Serde, Tokio, etc.)
- Review the crate's source code if possible
-
Audit dependencies
- Use
cargo treeto see all dependencies - Check for suspicious or unknown proc-macro dependencies
- Use
cargo-auditto check for known vulnerabilities
- Use
-
Use sandboxing
- Run untrusted code in Docker containers or VMs
- Use cargo-sandbox or similar tools (still experimental)
-
Be cautious with git clone + open workflow
- Review
Cargo.tomlfor proc-macro dependencies first - Search for
proc-macro = truein dependency sources - Consider using
--offlinemode for initial inspection
- Review
-
Disable automatic checks
- disable
rust-analyzerauto-check (⚠️ severly limits IDE functionality – not recommended) - Manually review code before running any cargo commands
- disable
- Open this project in GitHub Codespaces (uses the devcontainer)
- Create a fake
.envfile inclueless_import/:cd clueless_import && \ echo "API_KEY=super_secret_key_12345" > .env && \ echo "DATABASE_PASSWORD=hunter2" >> .env
- Open clueless_import/src/main.rs in VS Code
- Wait a few seconds for
rust-analyzerto activate - Check the created files:
cat compile_time_output.txt
- See your environment information and
.envcontents stolen!
Real-world attacks using malicious proc-macros:
- Supply chain attacks: Compromised popular macros could affect thousands of projects
- Typosquatting: Macros with names similar to popular crates (serde vs. serdi)
- Dependency confusion: Malicious internal crate names on public registries
- Targeted attacks: Macros designed to activate only in specific environments
By the time you've opened a Rust project with proc-macro dependencies in VS Code, it may already be too late.