- Published on
Rust Procedural Macros and Their Security Implications
- Authors

- Name
- Jonas Merhej
- @bonistech
⚠️ WARNING: SECURITY DEMONSTRATION ⚠️
This post shows how malicious code can execute on your machine simply by opening a Rust project in your IDE.
Overview
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.
1. What Are Macros?
Rust has two types of macros:
Declarative Macros (macro_rules!)
Pattern-matching macros that operate on syntax tokens (e.g., println!, vec!). These are generally safer
as they only perform text substitution.
Procedural Macros
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)
2. How Do They Generate Code?
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.
3. How Do Macros Execute Local Code?
Consider the following attribute macro example. The #[return_42] attribute macro appears innocent, but
executes malicious code at compile time:
#[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:
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.
4. What Is cargo expand?
cargo expand is a tool that shows you the code generated by macros:
cargo install cargo-expand
cd clueless_import
cargo expand
This reveals what macros actually generate, but it still executes the macro code to produce the expansion.
Important: Running cargo expand will trigger the malicious macro execution, so it's not safe for untrusted code!
5. Why Is Not Even cargo check Safe?
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.
6. How Often Does It Get Executed?
Procedural macros execute every time the compiler needs to process your code:
Automatic Execution Triggers:
-
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
Testing This Demo
⚠️ Only run this in a safe environment (Docker/VM) without real secrets!
- Clone the example repo:
git clone https://github.com/cre-mer/vulnerable-macro.git cd vulnerable-macro - 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.rsin 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!
How to Protect Yourself
-
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 (⚠️ severely limits IDE functionality — not recommended) - Manually review code before running any cargo commands
- Disable
Why This Matters
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 (
serdevs.serdi) - Dependency confusion: malicious internal crate names on public registries
- Targeted attacks: macros designed to activate only in specific environments
Conclusion
By the time you've opened a Rust project with proc-macro dependencies in VS Code, it may already be too late. The compile-time execution model of procedural macros is a powerful feature, but it fundamentally means that building code is equivalent to running code. Treat any untrusted Rust project with proc-macro dependencies the same way you would treat an untrusted binary — run it only in an isolated environment.