
Rust
- 245 installs
- 45 repo stars
- Updated December 6, 2025
- martinholovsky/claude-skills-generator
Implement performant Rust services, CLIs, and libraries with idiomatic patterns, error handling, and safe concurrency.
About
Provides Rust development guidance for building fast, memory-safe backends, CLIs, and libraries with idiomatic ownership patterns, async runtimes, robust error types, Cargo workflows, and testing practices suited to production services.
- Ownership, borrowing, and lifetimes
- Async Tokio service patterns
- Error handling with Result and anyhow
- Cargo workspace and crate layout
- Safe concurrency and performance tuning
Rust by the numbers
- 245 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #50 of 121 Rust skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/martinholovsky/claude-skills-generator --skill rustAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 245 |
|---|---|
| repo stars | ★ 45 |
| Last updated | December 6, 2025 |
| Repository | martinholovsky/claude-skills-generator ↗ |
What it does
Implement performant Rust services, CLIs, and libraries with idiomatic patterns, error handling, and safe concurrency.
Files
Rust Systems Programming Skill
File Organization
- SKILL.md: Core principles, patterns, and essential security (this file)
- references/security-examples.md: Complete CVE details and OWASP implementations
- references/advanced-patterns.md: Advanced Rust patterns and Tauri integration
Validation Gates
| Gate | Status | Notes |
|---|---|---|
| 0.1 Domain Expertise | PASSED | Ownership/borrowing, unsafe, FFI, async, Tauri commands |
| 0.2 Vulnerability Research | PASSED | 3+ CVEs documented (2025-11-20) |
| 0.5 Hallucination Check | PASSED | Examples tested against rustc 1.75+ |
| 0.11 File Organization | Split | MEDIUM-RISK, ~400 lines main + references |
---
1. Overview
Risk Level: MEDIUM
Justification: Rust provides memory safety through the borrow checker, but unsafe blocks, FFI boundaries, and command injection via std::process::Command present security risks.
You are an expert Rust systems programmer specializing in Tauri desktop application development. You write memory-safe, performant code following Rust idioms while understanding security boundaries between safe and unsafe code.
Core Expertise Areas
- Ownership, borrowing, and lifetime management
- Async Rust with Tokio runtime
- FFI and unsafe code safety
- Tauri command system and IPC
- Performance optimization and zero-cost abstractions
---
2. Core Responsibilities
Fundamental Principles
1. TDD First: Write tests before implementation to ensure correctness and prevent regressions 2. Performance Aware: Profile before optimizing, use zero-cost abstractions, avoid unnecessary allocations 3. Embrace the Type System: Encode invariants to prevent invalid states at compile time 4. Minimize Unsafe: Isolate unsafe code, document safety invariants, provide safe abstractions 5. Zero-Cost Abstractions: Write high-level code that compiles to efficient machine code 6. Error Handling with Result: Use Result for recoverable errors, panic only for bugs 7. Security at Boundaries: Validate all input at FFI and IPC boundaries
Decision Framework
| Situation | Approach |
|---|---|
| Shared ownership | Arc<T> (thread-safe) or Rc<T> (single-thread) |
| Interior mutability | Mutex<T>, RwLock<T>, or RefCell<T> |
| Performance-critical | Profile first, then consider unsafe optimizations |
| FFI interaction | Create safe wrapper types with validation |
| Error handling | Return Result<T, E> with custom error types |
---
3. Technical Foundation
Version Recommendations
| Category | Version | Notes |
|---|---|---|
| LTS/Stable | Rust 1.75+ | Minimum for Tauri 2.x |
| Recommended | Rust 1.82+ | Latest stable with security patches |
| Tauri | 2.0+ | Use 2.x for new projects |
| Tokio | 1.35+ | Async runtime |
Security Dependencies
[dependencies]
serde = { version = "1.0", features = ["derive"] }
validator = { version = "0.16", features = ["derive"] }
ring = "0.17" # Cryptography
argon2 = "0.5" # Password hashing
dunce = "1.0" # Safe path canonicalization
[dev-dependencies]
cargo-audit = "0.18" # Vulnerability scanning---
4. Implementation Workflow (TDD)
Step 1: Write Failing Test First
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_creation_valid_input() {
let input = UserInput { name: "Alice".to_string(), age: 30 };
let result = User::try_from(input);
assert!(result.is_ok());
assert_eq!(result.unwrap().name, "Alice");
}
#[test]
fn test_user_creation_rejects_empty_name() {
let input = UserInput { name: "".to_string(), age: 25 };
assert!(matches!(User::try_from(input), Err(AppError::Validation(_))));
}
#[tokio::test]
async fn test_async_state_concurrent_access() {
let state = AppState::new();
let state_clone = state.clone();
let handle = tokio::spawn(async move {
state_clone.update_user("1", User::new("Bob")).await
});
state.update_user("2", User::new("Alice")).await.unwrap();
handle.await.unwrap().unwrap();
assert!(state.get_user("1").await.is_some());
}
}Step 2: Implement Minimum Code to Pass
impl TryFrom<UserInput> for User {
type Error = AppError;
fn try_from(input: UserInput) -> Result<Self, Self::Error> {
if input.name.is_empty() {
return Err(AppError::Validation("Name cannot be empty".into()));
}
Ok(User { name: input.name, age: input.age })
}
}Step 3: Refactor and Verify
cargo test && cargo clippy -- -D warnings && cargo audit---
5. Implementation Patterns
Pattern 1: Secure Input Validation
Validate all Tauri command inputs using the validator crate with custom regex patterns.
use serde::Deserialize;
use validator::Validate;
#[derive(Deserialize, Validate)]
pub struct UserInput {
#[validate(length(min = 1, max = 100), regex(path = "SAFE_STRING_REGEX"))]
pub name: String,
#[validate(range(min = 0, max = 120))]
pub age: u8,
}
#[tauri::command]
pub async fn create_user(input: UserInput) -> Result<User, String> {
input.validate().map_err(|e| format!("Validation error: {}", e))?;
Ok(User::new(input))
}See `references/advanced-patterns.md` for complete validation patterns with regex definitions
Pattern 2: Safe Error Handling
Use thiserror for structured errors that serialize safely without exposing internals.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error")]
Database(#[from] sqlx::Error),
#[error("Validation failed: {0}")]
Validation(String),
#[error("Not found")]
NotFound,
}
impl serde::Serialize for AppError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer {
serializer.serialize_str(&self.to_string()) // Never expose internals
}
}Pattern 3: Secure File Operations
Prevent path traversal by canonicalizing paths and verifying containment.
pub fn safe_path_join(base: &Path, user_input: &str) -> Result<PathBuf, AppError> {
if user_input.contains("..") || user_input.contains("~") {
return Err(AppError::Validation("Invalid path characters".into()));
}
let canonical = dunce::canonicalize(base.join(user_input))
.map_err(|_| AppError::NotFound)?;
let base_canonical = dunce::canonicalize(base)
.map_err(|_| AppError::Internal(anyhow::anyhow!("Invalid base")))?;
if !canonical.starts_with(&base_canonical) {
return Err(AppError::Validation("Path traversal detected".into()));
}
Ok(canonical)
}Pattern 4: Safe Command Execution
Mitigate CVE-2024-24576 by using allowlists and avoiding shell execution.
pub fn safe_command(program: &str, args: &[&str]) -> Result<String, AppError> {
const ALLOWED: &[&str] = &["git", "cargo", "rustc"];
if !ALLOWED.contains(&program) {
return Err(AppError::Validation("Program not allowed".into()));
}
let output = Command::new(program).args(args).output()
.map_err(|e| AppError::Internal(e.into()))?;
if output.status.success() {
String::from_utf8(output.stdout).map_err(|e| AppError::Internal(e.into()))
} else {
Err(AppError::Internal(anyhow::anyhow!("Command failed")))
}
}Pattern 5: Safe Async State Management
Use Arc<RwLock<T>> for thread-safe shared state in Tauri applications.
pub struct AppState {
users: Arc<RwLock<HashMap<String, User>>>,
config: Arc<Config>,
}
impl AppState {
pub async fn get_user(&self, id: &str) -> Option<User> {
self.users.read().await.get(id).cloned()
}
pub async fn update_user(&self, id: &str, user: User) -> Result<(), AppError> {
self.users.write().await.insert(id.to_string(), user);
Ok(())
}
}See `references/advanced-patterns.md` for advanced state patterns and Tauri integration
---
6. Security Standards
5.1 Critical CVEs
| CVE ID | Severity | Description | Mitigation |
|---|---|---|---|
| CVE-2024-24576 | CRITICAL | Command injection via batch files (Windows) | Rust 1.77.2+, avoid shell |
| CVE-2024-43402 | HIGH | Incomplete fix for above | Rust 1.81.0+ |
| CVE-2021-28032 | HIGH | Multiple mutable references in unsafe | Audit unsafe blocks |
See `references/security-examples.md` for complete CVE details and mitigation code
5.2 OWASP Top 10 Mapping
| Category | Risk | Key Mitigations |
|---|---|---|
| A01 Broken Access Control | MEDIUM | Validate permissions in Tauri commands |
| A03 Injection | HIGH | Command without shell, parameterized queries |
| A04 Insecure Design | MEDIUM | Type system to enforce invariants |
| A06 Vulnerable Components | HIGH | Run cargo-audit regularly |
5.3 Input Validation Strategy
Four-layer approach: Type system newtypes -> Schema validation (serde/validator) -> Business logic -> Output encoding
pub struct Email(String); // Newtype for validated input
impl Email {
pub fn new(s: &str) -> Result<Self, ValidationError> {
if validator::validate_email(s) { Ok(Self(s.to_string())) }
else { Err(ValidationError::InvalidEmail) }
}
}5.4 Secrets Management
// Load from environment or tauri-plugin-store with encryption
fn get_api_key() -> Result<String, AppError> {
std::env::var("API_KEY")
.map_err(|_| AppError::Configuration("API_KEY not set".into()))
}See `references/security-examples.md` for secure storage patterns
---
7. Performance Patterns
Pattern 1: Zero-Copy Operations
Bad: data.to_vec() then iterate - Good: Return iterator with lifetime
// Bad: fn process(data: &[u8]) -> Vec<u8> { data.to_vec().iter().map(|b| b+1).collect() }
fn process(data: &[u8]) -> impl Iterator<Item = u8> + '_ {
data.iter().map(|b| b + 1) // No allocation
}Pattern 2: Iterator Chains Over Loops
Bad: Manual loop with push - Good: Iterator chain (lazy, fused)
fn filter_transform(items: &[Item]) -> Vec<String> {
items.iter().filter(|i| i.is_valid()).map(|i| i.name.to_uppercase()).collect()
}Pattern 3: Memory Pooling for Frequent Allocations
Bad: Vec::with_capacity() in hot path - Good: Object pool
static BUFFER_POOL: Lazy<Pool<Vec<u8>>> = Lazy::new(|| Pool::new(32, || Vec::with_capacity(1024)));
async fn handle_request(data: &[u8]) -> Vec<u8> {
let mut buffer = BUFFER_POOL.pull(|| Vec::with_capacity(1024));
buffer.clear(); process(&mut buffer, data); buffer.to_vec()
}Pattern 4: Async Runtime Selection
Bad: CPU work on async - Good: spawn_blocking for CPU-bound
async fn hash_password(password: String) -> Result<String, AppError> {
tokio::task::spawn_blocking(move || {
argon2::hash_encoded(password.as_bytes(), &salt, &config)
.map_err(|e| AppError::Internal(e.into()))
}).await?
}Pattern 5: Avoid Allocations in Hot Paths
Bad: println! allocates - Good: write! to preallocated buffer
fn log_metric(buffer: &mut Vec<u8>, name: &str, value: u64) {
buffer.clear();
write!(buffer, "{}: {}", name, value).unwrap();
std::io::stdout().write_all(buffer).unwrap();
}---
8. Testing & Validation
Security Testing Commands
cargo audit # Dependency vulnerabilities
cargo +nightly careful test # Memory safety checking
cargo clippy -- -D warnings # Lint with security warningsUnit Test Pattern
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_traversal_blocked() {
let base = Path::new("/app/data");
assert!(safe_path_join(base, "../etc/passwd").is_err());
assert!(safe_path_join(base, "user/file.txt").is_ok());
}
#[test]
fn test_command_allowlist() {
assert!(safe_command("rm", &["-rf", "/"]).is_err());
assert!(safe_command("git", &["status"]).is_ok());
}
}See `references/advanced-patterns.md` for fuzzing and integration test patterns
---
9. Common Mistakes & Anti-Patterns
| Anti-Pattern | Problem | Solution |
|---|---|---|
.unwrap() in production | Panics crash app | Use ? with Result |
| Unsafe without docs | Unverified invariants | Add // SAFETY: comments |
| Shell command execution | Injection vulnerability | Use Command::new() directly |
| Ignoring Clippy | Missed security lints | Run cargo clippy -- -D warnings |
| Hardcoded credentials | Secrets in code | Use env vars or secure storage |
// NEVER: Shell injection
Command::new("sh").arg("-c").arg(format!("echo {}", user_input));
// ALWAYS: Direct execution
Command::new("echo").arg(user_input);---
10. Pre-Implementation Checklist
Phase 1: Before Writing Code
- [ ] Write failing tests that define expected behavior
- [ ] Review relevant CVEs for the feature area
- [ ] Identify security boundaries (FFI, IPC, file system)
- [ ] Plan error handling strategy with Result types
- [ ] Check dependencies with
cargo audit
Phase 2: During Implementation
- [ ] Run tests after each significant change
- [ ] Document all unsafe blocks with
// SAFETY:comments - [ ] Validate inputs at all boundaries (Tauri commands, FFI)
- [ ] Use type system to enforce invariants (newtypes)
- [ ] Apply performance patterns (zero-copy, iterators)
- [ ] Ensure error messages don't leak internal details
Phase 3: Before Committing
- [ ]
cargo test- all tests pass - [ ]
cargo clippy -- -D warnings- no warnings - [ ]
cargo audit- zero HIGH/CRITICAL vulnerabilities - [ ] No hardcoded secrets (grep for "password", "secret", "key")
- [ ] Path operations use canonicalization and containment checks
- [ ] Command execution uses allowlist, no shell
- [ ] Panic handler configured for graceful shutdown
- [ ] Logging configured (no secrets in logs)
---
11. Summary
Your goal is to create Rust code that is:
- Memory Safe: Leverage the borrow checker, minimize unsafe
- Type Safe: Use the type system to prevent invalid states
- Performant: Zero-cost abstractions, profile before optimizing
- Secure: Validate at boundaries, handle errors safely
Critical Security Reminders: 1. Upgrade to Rust 1.81.0+ to fix command injection CVEs 2. Run cargo-audit in CI/CD pipeline 3. Document SAFETY invariants for all unsafe blocks 4. Never use shell execution with user input 5. Canonicalize and validate all file paths
For detailed examples and advanced patterns, see the `references/` directory
Rust Advanced Patterns Reference
Tauri Integration Patterns
State Management
use std::sync::Arc;
use tokio::sync::RwLock;
use tauri::State;
pub struct AppState {
pub db: Arc<DatabasePool>,
pub config: Arc<RwLock<Config>>,
pub cache: Arc<Cache>,
}
#[tauri::command]
async fn get_config(state: State<'_, AppState>) -> Result<Config, String> {
let config = state.config.read().await;
Ok(config.clone())
}
#[tauri::command]
async fn update_config(
new_config: Config,
state: State<'_, AppState>,
) -> Result<(), String> {
// Validate before updating
new_config.validate()?;
let mut config = state.config.write().await;
*config = new_config;
Ok(())
}Event System
use tauri::{AppHandle, Manager};
// Emit events to frontend
pub fn emit_progress(app: &AppHandle, progress: f64) -> Result<(), tauri::Error> {
app.emit_all("progress", progress)
}
// Listen for events from frontend
#[tauri::command]
async fn start_task(app: AppHandle) -> Result<(), String> {
tokio::spawn(async move {
for i in 0..100 {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let _ = emit_progress(&app, i as f64 / 100.0);
}
});
Ok(())
}Plugin Development
use tauri::{
plugin::{Builder, TauriPlugin},
Runtime,
};
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("my-plugin")
.invoke_handler(tauri::generate_handler![
plugin_command_1,
plugin_command_2,
])
.setup(|app| {
// Initialize plugin state
app.manage(PluginState::default());
Ok(())
})
.build()
}---
Performance Optimization
Zero-Copy Parsing
use std::borrow::Cow;
// Avoid unnecessary allocations
pub fn process_data(input: &str) -> Cow<'_, str> {
if input.contains("replace_me") {
// Only allocate when needed
Cow::Owned(input.replace("replace_me", "replaced"))
} else {
// Zero-copy when no changes needed
Cow::Borrowed(input)
}
}Async Streaming
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::fs::File;
// Stream large files without loading into memory
pub async fn process_large_file(path: &str) -> Result<(), Error> {
let file = File::open(path).await?;
let reader = BufReader::new(file);
let mut lines = reader.lines();
while let Some(line) = lines.next_line().await? {
process_line(&line).await?;
}
Ok(())
}Connection Pooling
use sqlx::postgres::PgPoolOptions;
pub async fn create_pool(database_url: &str) -> Result<PgPool, Error> {
PgPoolOptions::new()
.max_connections(20)
.min_connections(5)
.acquire_timeout(std::time::Duration::from_secs(30))
.idle_timeout(std::time::Duration::from_secs(600))
.connect(database_url)
.await
}---
Type-Safe Patterns
Newtype Pattern
// Prevent mixing up IDs
pub struct UserId(pub i64);
pub struct OrderId(pub i64);
impl UserId {
pub fn new(id: i64) -> Result<Self, ValidationError> {
if id <= 0 {
return Err(ValidationError::InvalidId);
}
Ok(Self(id))
}
}
// Compiler prevents: get_user(order_id) - wrong type!
async fn get_user(user_id: UserId) -> Result<User, Error> {
// ...
}Builder Pattern
#[derive(Default)]
pub struct RequestBuilder {
url: Option<String>,
timeout: Option<Duration>,
headers: HashMap<String, String>,
}
impl RequestBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
pub fn build(self) -> Result<Request, BuildError> {
let url = self.url.ok_or(BuildError::MissingUrl)?;
Ok(Request {
url,
timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
headers: self.headers,
})
}
}Typestate Pattern
// Compile-time state machine
pub struct Connection<S> {
state: S,
// ...
}
pub struct Disconnected;
pub struct Connected;
pub struct Authenticated;
impl Connection<Disconnected> {
pub fn new() -> Self {
Connection { state: Disconnected }
}
pub async fn connect(self, addr: &str) -> Result<Connection<Connected>, Error> {
// Connect logic...
Ok(Connection { state: Connected })
}
}
impl Connection<Connected> {
pub async fn authenticate(self, token: &str) -> Result<Connection<Authenticated>, Error> {
// Auth logic...
Ok(Connection { state: Authenticated })
}
}
impl Connection<Authenticated> {
pub async fn send(&self, data: &[u8]) -> Result<(), Error> {
// Only authenticated connections can send
Ok(())
}
}
// Usage:
// Connection::new()
// .connect("localhost:8080").await?
// .authenticate("token").await?
// .send(b"data").await?;---
Async Patterns
Graceful Shutdown
use tokio::signal;
use tokio::sync::broadcast;
pub async fn run_with_shutdown(app: App) -> Result<(), Error> {
let (shutdown_tx, _) = broadcast::channel(1);
let server = tokio::spawn({
let mut shutdown_rx = shutdown_tx.subscribe();
async move {
tokio::select! {
result = app.run() => result,
_ = shutdown_rx.recv() => Ok(()),
}
}
});
// Wait for shutdown signal
signal::ctrl_c().await?;
tracing::info!("Shutdown signal received");
// Notify all tasks
let _ = shutdown_tx.send(());
// Wait for graceful shutdown with timeout
tokio::time::timeout(
std::time::Duration::from_secs(30),
server
).await??
}Rate Limiting
use std::sync::Arc;
use tokio::sync::Semaphore;
use tokio::time::{interval, Duration};
pub struct RateLimiter {
semaphore: Arc<Semaphore>,
}
impl RateLimiter {
pub fn new(permits_per_second: usize) -> Self {
let semaphore = Arc::new(Semaphore::new(permits_per_second));
// Replenish permits every second
let sem = semaphore.clone();
tokio::spawn(async move {
let mut ticker = interval(Duration::from_secs(1));
loop {
ticker.tick().await;
let to_add = permits_per_second.saturating_sub(sem.available_permits());
sem.add_permits(to_add);
}
});
Self { semaphore }
}
pub async fn acquire(&self) -> Result<(), Error> {
self.semaphore
.acquire()
.await
.map_err(|_| Error::RateLimited)?;
Ok(())
}
}---
Testing Patterns
Property-Based Testing
#[cfg(test)]
mod tests {
use proptest::prelude::*;
proptest! {
#[test]
fn test_path_join_never_escapes(
base in "[a-z]{1,10}",
input in "[a-z0-9_\\-]{1,20}"
) {
let base_path = std::path::Path::new(&base);
if let Ok(result) = safe_path_join(base_path, &input) {
// Result must always start with base
prop_assert!(result.starts_with(base_path));
}
}
#[test]
fn test_serialization_roundtrip(config: Config) {
let json = serde_json::to_string(&config).unwrap();
let parsed: Config = serde_json::from_str(&json).unwrap();
prop_assert_eq!(config, parsed);
}
}
}Async Test Fixtures
#[cfg(test)]
mod tests {
use sqlx::PgPool;
use once_cell::sync::Lazy;
static TEST_DB: Lazy<PgPool> = Lazy::new(|| {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async {
create_test_pool().await.unwrap()
})
});
#[tokio::test]
async fn test_user_creation() {
let pool = &*TEST_DB;
// Test with real database
let user = create_user(pool, "test@example.com").await.unwrap();
assert_eq!(user.email, "test@example.com");
// Cleanup
delete_user(pool, user.id).await.unwrap();
}
}Rust Security Examples Reference
CVE Details and Mitigations
CVE-2024-24576: Command Injection via Batch Files (Windows)
Severity: CRITICAL (CVSS 10.0) Affected: Rust < 1.77.2 on Windows CWE: CWE-78 (OS Command Injection)
Description: The Rust standard library did not properly escape arguments when invoking batch files (.bat, .cmd) on Windows using std::process::Command. Attackers could execute arbitrary shell commands.
Vulnerable Code:
// VULNERABLE: Arguments not properly escaped
use std::process::Command;
fn run_batch(user_arg: &str) {
Command::new("script.bat")
.arg(user_arg) // If user_arg = "foo & malicious.exe", injection occurs
.spawn();
}Mitigation:
// FIXED: Upgrade Rust and validate input
use std::process::Command;
fn run_batch(user_arg: &str) -> Result<(), String> {
// Input validation - reject shell metacharacters
if user_arg.chars().any(|c| matches!(c, '&' | '|' | ';' | '$' | '`' | '(' | ')')) {
return Err("Invalid characters in argument".into());
}
// Use allowlist approach
if !user_arg.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
return Err("Argument contains disallowed characters".into());
}
Command::new("script.bat")
.arg(user_arg)
.spawn()
.map_err(|e| e.to_string())?;
Ok(())
}
// BEST: Avoid batch files entirely - use direct execution
fn safe_alternative() {
Command::new("program.exe")
.args(["--option", "value"])
.spawn();
}---
CVE-2024-43402: Incomplete Fix for Command Injection
Severity: HIGH Affected: Rust 1.77.2 to < 1.81.0 on Windows CWE: CWE-78 (OS Command Injection)
Description: The fix for CVE-2024-24576 was incomplete. Batch files with trailing whitespace or periods could bypass escaping.
Mitigation:
// Upgrade to Rust 1.81.0+
// Additional defense: normalize filenames
fn safe_batch_call(batch_name: &str, args: &[&str]) -> Result<(), String> {
// Strip trailing whitespace/periods that Windows ignores
let normalized = batch_name.trim_end_matches(|c| c == ' ' || c == '.');
// Validate batch file exists with exact name
if !std::path::Path::new(&format!("{}.bat", normalized)).exists() {
return Err("Batch file not found".into());
}
Command::new(format!("{}.bat", normalized))
.args(args)
.spawn()
.map_err(|e| e.to_string())?;
Ok(())
}---
CVE-2021-28032: Multiple Mutable References
Severity: HIGH Affected: Various crates using unsafe incorrectly CWE: CWE-119 (Buffer Errors), CWE-416 (Use After Free)
Description: Unsafe code created multiple mutable references to the same memory, violating Rust's aliasing rules and potentially causing undefined behavior.
Vulnerable Pattern:
// VULNERABLE: Creates multiple mutable references
unsafe fn bad_split(slice: &mut [u8]) -> (&mut [u8], &mut [u8]) {
let ptr = slice.as_mut_ptr();
let len = slice.len();
// Both slices can modify the same memory!
(
std::slice::from_raw_parts_mut(ptr, len),
std::slice::from_raw_parts_mut(ptr, len),
)
}Safe Implementation:
// SAFE: Use split_at_mut which enforces non-overlapping
fn safe_split(slice: &mut [u8], mid: usize) -> (&mut [u8], &mut [u8]) {
slice.split_at_mut(mid)
}
// If unsafe is required, document invariants
unsafe fn documented_unsafe(slice: &mut [u8], mid: usize) -> (&mut [u8], &mut [u8]) {
let ptr = slice.as_mut_ptr();
let len = slice.len();
assert!(mid <= len, "mid out of bounds");
// SAFETY: The two slices are non-overlapping:
// - First slice: [0, mid)
// - Second slice: [mid, len)
// Both are within the original allocation and properly aligned.
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}---
OWASP Top 10 2025 Complete Examples
A01: Broken Access Control
// VULNERABLE: No authorization check
#[tauri::command]
async fn delete_user(user_id: String) -> Result<(), String> {
db.delete_user(&user_id).await?;
Ok(())
}
// SECURE: Verify permissions
#[tauri::command]
async fn delete_user(
user_id: String,
state: State<'_, AppState>,
) -> Result<(), String> {
let current_user = state.get_current_user()?;
// Check authorization
if current_user.id != user_id && !current_user.is_admin {
return Err("Not authorized to delete this user".into());
}
db.delete_user(&user_id).await?;
// Audit log
tracing::info!(
action = "delete_user",
target_user = %user_id,
performed_by = %current_user.id,
"User deleted"
);
Ok(())
}A03: Injection
// VULNERABLE: SQL injection
async fn get_user(name: &str) -> Result<User, Error> {
sqlx::query(&format!("SELECT * FROM users WHERE name = '{}'", name))
.fetch_one(&pool)
.await
}
// SECURE: Parameterized query
async fn get_user(name: &str) -> Result<User, Error> {
sqlx::query_as!(
User,
"SELECT id, name, email FROM users WHERE name = $1",
name
)
.fetch_one(&pool)
.await
}
// VULNERABLE: Command injection
fn ping(host: &str) {
Command::new("sh")
.args(["-c", &format!("ping -c 1 {}", host)])
.spawn();
}
// SECURE: Direct execution with validation
fn ping(host: &str) -> Result<(), String> {
// Validate IP/hostname format
let ip_regex = regex::Regex::new(r"^[\d\.]+$|^[\w\-\.]+$").unwrap();
if !ip_regex.is_match(host) {
return Err("Invalid host format".into());
}
Command::new("ping")
.args(["-c", "1", host])
.spawn()
.map_err(|e| e.to_string())?;
Ok(())
}A04: Insecure Design
// VULNERABLE: Password reset token predictable
fn generate_reset_token() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
format!("{:x}", secs) // Predictable!
}
// SECURE: Cryptographically random token
fn generate_reset_token() -> String {
use rand::Rng;
let mut rng = rand::thread_rng();
let token: [u8; 32] = rng.gen();
hex::encode(token)
}A05: Security Misconfiguration
# tauri.conf.json - Restrict capabilities
{
"tauri": {
"security": {
"csp": "default-src 'self'; script-src 'self'",
"dangerousDisableAssetCspModification": false
},
"allowlist": {
"all": false, // NEVER enable all
"fs": {
"scope": ["$APPDATA/*"], // Restrict to app directory
"readFile": true,
"writeFile": true
},
"shell": {
"open": false, // Disable if not needed
"execute": false
}
}
}
}A06: Vulnerable and Outdated Components
# Regular dependency auditing
cargo audit
# In CI/CD pipeline
cargo audit --deny warnings
# Keep dependencies updated
cargo update
# Check for outdated dependencies
cargo outdated---
Additional Security Patterns
Safe FFI Wrapper
// External C library
extern "C" {
fn unsafe_c_function(ptr: *const u8, len: usize) -> i32;
}
// Safe Rust wrapper
pub fn safe_wrapper(data: &[u8]) -> Result<i32, Error> {
// Validate input
if data.is_empty() {
return Err(Error::InvalidInput("Empty data"));
}
if data.len() > MAX_SIZE {
return Err(Error::InvalidInput("Data too large"));
}
// SAFETY: We verified data is not empty and within size limits.
// The C function only reads from the pointer for len bytes.
let result = unsafe {
unsafe_c_function(data.as_ptr(), data.len())
};
if result < 0 {
Err(Error::CFunction(result))
} else {
Ok(result)
}
}Secure Deserialization
use serde::Deserialize;
// Limit deserialization depth/size to prevent DoS
#[derive(Deserialize)]
#[serde(deny_unknown_fields)] // Reject unexpected fields
pub struct Config {
#[serde(deserialize_with = "validate_size")]
pub buffer_size: usize,
pub timeout_seconds: u64,
}
fn validate_size<'de, D>(deserializer: D) -> Result<usize, D::Error>
where
D: serde::Deserializer<'de>,
{
let size = usize::deserialize(deserializer)?;
if size > 1024 * 1024 { // 1MB limit
return Err(serde::de::Error::custom("buffer_size too large"));
}
Ok(size)
}