Agent skill
rust-security
Rust security best practices and vulnerability prevention. Use when handling user input, authentication, cryptography, secrets management, network security, or conducting security reviews.
Stars
163
Forks
31
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/security/rust-security-vanyastaff-paramdef
SKILL.md
Rust Security Best Practices
Comprehensive guide for writing secure Rust code.
Dependency Security
Audit Dependencies
bash
# Install and run cargo-audit
cargo install cargo-audit
cargo audit
# Check for unmaintained crates
cargo audit --deny unmaintained
# Generate lockfile advisories
cargo audit --json > audit-report.json
# Deny specific advisories in CI
cargo audit --deny RUSTSEC-2023-0001
Cargo.toml Security
toml
[package]
# Pin exact versions for security-critical deps
rust-version = "1.85"
[dependencies]
# Use caret for flexibility, but audit regularly
ring = "0.17"
rustls = "0.23"
# Avoid yanked versions
# cargo update will warn about these
[features]
# Disable default features, enable only what you need
default = []
cargo-deny Configuration
toml
# deny.toml
[advisories]
db-path = "~/.cargo/advisory-db"
vulnerability = "deny"
unmaintained = "warn"
yanked = "deny"
[licenses]
allow = ["MIT", "Apache-2.0", "BSD-3-Clause"]
copyleft = "deny"
[bans]
multiple-versions = "warn"
deny = [
# Known problematic crates
{ name = "openssl" }, # Prefer rustls
]
[sources]
unknown-registry = "deny"
unknown-git = "deny"
Input Validation
String Validation
rust
use std::borrow::Cow;
/// Validates and sanitizes user input.
pub fn validate_username(input: &str) -> Result<Cow<'_, str>, ValidationError> {
// Length limits
if input.is_empty() {
return Err(ValidationError::Empty);
}
if input.len() > 64 {
return Err(ValidationError::TooLong { max: 64 });
}
// Character whitelist
if !input.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') {
return Err(ValidationError::InvalidCharacters);
}
// Reserved names
const RESERVED: &[&str] = &["admin", "root", "system", "null"];
if RESERVED.contains(&input.to_lowercase().as_str()) {
return Err(ValidationError::Reserved);
}
Ok(Cow::Borrowed(input))
}
Path Traversal Prevention
rust
use std::path::{Path, PathBuf};
/// Safely joins a user-provided path to a base directory.
pub fn safe_join(base: &Path, user_path: &str) -> Result<PathBuf, SecurityError> {
// Reject absolute paths
let user_path = Path::new(user_path);
if user_path.is_absolute() {
return Err(SecurityError::AbsolutePathRejected);
}
// Reject path traversal
for component in user_path.components() {
match component {
std::path::Component::ParentDir => {
return Err(SecurityError::PathTraversal);
}
std::path::Component::Normal(_) => {}
_ => return Err(SecurityError::InvalidPathComponent),
}
}
let full_path = base.join(user_path);
// Verify the result is still under base
let canonical = full_path.canonicalize()
.map_err(|_| SecurityError::PathResolutionFailed)?;
let base_canonical = base.canonicalize()
.map_err(|_| SecurityError::PathResolutionFailed)?;
if !canonical.starts_with(&base_canonical) {
return Err(SecurityError::PathEscape);
}
Ok(canonical)
}
SQL Injection Prevention
rust
use sqlx::{query, query_as, PgPool};
// BAD - string interpolation
async fn bad_query(pool: &PgPool, user_id: &str) {
let query = format!("SELECT * FROM users WHERE id = '{}'", user_id);
// VULNERABLE TO SQL INJECTION
}
// GOOD - parameterized queries
async fn good_query(pool: &PgPool, user_id: &str) -> Result<User, Error> {
query_as!(User, "SELECT * FROM users WHERE id = $1", user_id)
.fetch_one(pool)
.await
.map_err(Error::from)
}
// GOOD - using sqlx macros (compile-time checked)
async fn safe_query(pool: &PgPool, user_id: Uuid) -> Result<User, Error> {
sqlx::query_as!(
User,
r#"SELECT id, name, email FROM users WHERE id = $1"#,
user_id
)
.fetch_one(pool)
.await
.map_err(Error::from)
}
Command Injection Prevention
rust
use std::process::Command;
// BAD - shell interpolation
fn bad_command(filename: &str) {
Command::new("sh")
.arg("-c")
.arg(format!("cat {}", filename)) // VULNERABLE
.output();
}
// GOOD - direct argument passing
fn good_command(filename: &str) -> std::io::Result<Vec<u8>> {
let output = Command::new("cat")
.arg(filename) // Passed as single argument, no shell interpretation
.output()?;
Ok(output.stdout)
}
// BETTER - avoid shell entirely when possible
fn read_file(filename: &str) -> std::io::Result<Vec<u8>> {
std::fs::read(filename)
}
Authentication & Authorization
Password Hashing
rust
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
/// Hashes a password using Argon2id.
pub fn hash_password(password: &str) -> Result<String, AuthError> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
argon2
.hash_password(password.as_bytes(), &salt)
.map(|hash| hash.to_string())
.map_err(|_| AuthError::HashingFailed)
}
/// Verifies a password against a hash.
pub fn verify_password(password: &str, hash: &str) -> Result<bool, AuthError> {
let parsed_hash = PasswordHash::new(hash)
.map_err(|_| AuthError::InvalidHash)?;
Ok(Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
}
Token Generation
rust
use rand::{rngs::OsRng, RngCore};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
/// Generates a cryptographically secure random token.
pub fn generate_token(length: usize) -> String {
let mut bytes = vec![0u8; length];
OsRng.fill_bytes(&mut bytes);
URL_SAFE_NO_PAD.encode(&bytes)
}
/// Generates a session ID.
pub fn generate_session_id() -> String {
generate_token(32) // 256 bits of entropy
}
/// Generates a CSRF token.
pub fn generate_csrf_token() -> String {
generate_token(32)
}
Constant-Time Comparison
rust
use subtle::ConstantTimeEq;
/// Compares two tokens in constant time to prevent timing attacks.
pub fn secure_compare(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
a.ct_eq(b).into()
}
// For string tokens
pub fn secure_compare_str(a: &str, b: &str) -> bool {
secure_compare(a.as_bytes(), b.as_bytes())
}
Secrets Management
Secure String Handling
rust
use secrecy::{ExposeSecret, Secret};
use zeroize::Zeroize;
/// A password that is zeroized on drop.
pub struct Password(Secret<String>);
impl Password {
pub fn new(password: String) -> Self {
Self(Secret::new(password))
}
pub fn expose(&self) -> &str {
self.0.expose_secret()
}
}
// For custom types
#[derive(Zeroize)]
#[zeroize(drop)]
pub struct ApiKey {
key: String,
}
impl Drop for ApiKey {
fn drop(&mut self) {
self.zeroize();
}
}
Environment Variables
rust
use secrecy::Secret;
use std::env;
/// Loads secrets from environment variables.
pub struct Secrets {
pub database_url: Secret<String>,
pub api_key: Secret<String>,
pub jwt_secret: Secret<String>,
}
impl Secrets {
pub fn from_env() -> Result<Self, ConfigError> {
Ok(Self {
database_url: Secret::new(
env::var("DATABASE_URL")
.map_err(|_| ConfigError::MissingEnv("DATABASE_URL"))?
),
api_key: Secret::new(
env::var("API_KEY")
.map_err(|_| ConfigError::MissingEnv("API_KEY"))?
),
jwt_secret: Secret::new(
env::var("JWT_SECRET")
.map_err(|_| ConfigError::MissingEnv("JWT_SECRET"))?
),
})
}
}
// NEVER log secrets
impl std::fmt::Debug for Secrets {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Secrets")
.field("database_url", &"[REDACTED]")
.field("api_key", &"[REDACTED]")
.field("jwt_secret", &"[REDACTED]")
.finish()
}
}
Cryptography
Use High-Level Libraries
rust
// GOOD - use established, audited libraries
use ring::rand::SecureRandom;
use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM};
pub struct Encryptor {
key: LessSafeKey,
}
impl Encryptor {
pub fn new(key_bytes: &[u8; 32]) -> Result<Self, CryptoError> {
let unbound_key = UnboundKey::new(&AES_256_GCM, key_bytes)
.map_err(|_| CryptoError::InvalidKey)?;
Ok(Self {
key: LessSafeKey::new(unbound_key),
})
}
pub fn encrypt(&self, plaintext: &[u8], nonce: &[u8; 12]) -> Result<Vec<u8>, CryptoError> {
let nonce = Nonce::assume_unique_for_key(*nonce);
let mut in_out = plaintext.to_vec();
self.key
.seal_in_place_append_tag(nonce, Aad::empty(), &mut in_out)
.map_err(|_| CryptoError::EncryptionFailed)?;
Ok(in_out)
}
}
TLS Configuration
rust
use rustls::{ClientConfig, RootCertStore};
use std::sync::Arc;
/// Creates a secure TLS client configuration.
pub fn create_tls_config() -> Result<Arc<ClientConfig>, TlsError> {
let mut root_store = RootCertStore::empty();
// Use webpki-roots for trusted CAs
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
Ok(Arc::new(config))
}
// For reqwest
use reqwest::Client;
pub fn create_secure_client() -> Result<Client, reqwest::Error> {
Client::builder()
.use_rustls_tls()
.min_tls_version(reqwest::tls::Version::TLS_1_2)
.https_only(true)
.build()
}
Rate Limiting & DoS Prevention
Rate Limiter
rust
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
pub struct RateLimiter {
requests: Mutex<HashMap<String, Vec<Instant>>>,
max_requests: usize,
window: Duration,
}
impl RateLimiter {
pub fn new(max_requests: usize, window: Duration) -> Self {
Self {
requests: Mutex::new(HashMap::new()),
max_requests,
window,
}
}
pub fn check(&self, key: &str) -> Result<(), RateLimitError> {
let mut requests = self.requests.lock().unwrap();
let now = Instant::now();
let cutoff = now - self.window;
let timestamps = requests.entry(key.to_string()).or_default();
// Remove old timestamps
timestamps.retain(|&t| t > cutoff);
if timestamps.len() >= self.max_requests {
return Err(RateLimitError::TooManyRequests);
}
timestamps.push(now);
Ok(())
}
}
Request Size Limits
rust
use axum::{
body::Body,
extract::Request,
middleware::Next,
response::Response,
};
use http::StatusCode;
const MAX_BODY_SIZE: u64 = 1024 * 1024; // 1 MB
pub async fn limit_body_size(
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let content_length = request
.headers()
.get(http::header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok());
if let Some(length) = content_length {
if length > MAX_BODY_SIZE {
return Err(StatusCode::PAYLOAD_TOO_LARGE);
}
}
Ok(next.run(request).await)
}
Logging Security
Safe Logging
rust
use tracing::{info, warn, error, instrument};
// BAD - logs sensitive data
fn bad_login(username: &str, password: &str) {
info!("Login attempt: user={}, pass={}", username, password);
}
// GOOD - redact sensitive fields
#[instrument(skip(password), fields(username = %username))]
fn good_login(username: &str, password: &str) -> Result<(), AuthError> {
info!("Login attempt");
// ... authentication logic
Ok(())
}
// Use skip for sensitive parameters
#[instrument(skip(api_key, request_body))]
async fn api_call(
endpoint: &str,
api_key: &str,
request_body: &[u8],
) -> Result<Response, Error> {
info!("API call to {}", endpoint);
// ...
}
// Redact in error messages
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error("Invalid credentials")] // Don't say which one
InvalidCredentials,
#[error("Account locked")]
AccountLocked,
}
Security Headers
rust
use axum::{
middleware::Next,
response::Response,
http::{Request, header},
};
pub async fn security_headers<B>(
request: Request<B>,
next: Next<B>,
) -> Response {
let mut response = next.run(request).await;
let headers = response.headers_mut();
// Prevent XSS
headers.insert(
header::X_CONTENT_TYPE_OPTIONS,
"nosniff".parse().unwrap(),
);
// Prevent clickjacking
headers.insert(
header::X_FRAME_OPTIONS,
"DENY".parse().unwrap(),
);
// HSTS
headers.insert(
header::STRICT_TRANSPORT_SECURITY,
"max-age=31536000; includeSubDomains".parse().unwrap(),
);
// CSP
headers.insert(
header::CONTENT_SECURITY_POLICY,
"default-src 'self'".parse().unwrap(),
);
response
}
Security Checklist
Before Release
-
cargo audit- no known vulnerabilities -
cargo deny check- license and dependency review - All user input validated and sanitized
- SQL queries parameterized
- No shell command interpolation
- Secrets not in code or logs
- TLS 1.2+ enforced
- Rate limiting enabled
- Security headers configured
- Error messages don't leak internals
Crate Recommendations
| Purpose | Recommended Crate |
|---|---|
| Password hashing | argon2 |
| Cryptography | ring, rustls |
| TLS | rustls (not openssl) |
| Secrets | secrecy, zeroize |
| Random | rand with OsRng |
| Timing-safe compare | subtle |
| HTTP client | reqwest with rustls |
| SQL | sqlx with compile-time checks |
Didn't find tool you were looking for?