| name | rust-security |
| description | Rust security best practices and vulnerability prevention. Use when handling user input, authentication, cryptography, secrets management, network security, or conducting security reviews. |
| allowed-tools | Read, Write, Edit, Bash, Grep, Glob |
Rust Security Best Practices
Comprehensive guide for writing secure Rust code.
Dependency Security
Audit Dependencies
cargo install cargo-audit
cargo audit
cargo audit --deny unmaintained
cargo audit --json > audit-report.json
cargo audit --deny RUSTSEC-2023-0001
Cargo.toml Security
[package]
rust-version = "1.85"
[dependencies]
ring = "0.17"
rustls = "0.23"
[features]
default = []
cargo-deny Configuration
[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 = [
{ name = "openssl" },
]
[sources]
unknown-registry = "deny"
unknown-git = "deny"
Input Validation
String Validation
use std::borrow::Cow;
pub fn validate_username(input: &str) -> Result<Cow<'_, str>, ValidationError> {
if input.is_empty() {
return Err(ValidationError::Empty);
}
if input.len() > 64 {
return Err(ValidationError::TooLong { max: 64 });
}
if !input.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') {
return Err(ValidationError::InvalidCharacters);
}
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
use std::path::{Path, PathBuf};
pub fn safe_join(base: &Path, user_path: &str) -> Result<PathBuf, SecurityError> {
let user_path = Path::new(user_path);
if user_path.is_absolute() {
return Err(SecurityError::AbsolutePathRejected);
}
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);
let canonical = full_path.canonicalize()
.map_err(|_| SecurityError::PathResolutionFailed)?;
let base_canonical = base.canonicalize()
.map_err(|_| SecurityError::PathResolutionFailed)?;
if !canonical.(&base_canonical) {
(SecurityError::PathEscape);
}
(canonical)
}
SQL Injection Prevention
use sqlx::{query, query_as, PgPool};
async fn bad_query(pool: &PgPool, user_id: &str) {
let query = format!("SELECT * FROM users WHERE id = '{}'", user_id);
}
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)
}
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
use std::process::Command;
fn bad_command(filename: &str) {
Command::new("sh")
.arg("-c")
.arg(format!("cat {}", filename))
.output();
}
fn good_command(filename: &str) -> std::io::Result<Vec<u8>> {
let output = Command::new("cat")
.arg(filename)
.output()?;
Ok(output.stdout)
}
fn read_file(filename: &str) -> std::io::Result<Vec<u8>> {
std::fs::read(filename)
}
Authentication & Authorization
Password Hashing
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
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)
}
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
use rand::{rngs::OsRng, RngCore};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
pub fn generate_token(length: usize) -> String {
let mut bytes = vec![0u8; length];
OsRng.fill_bytes(&mut bytes);
URL_SAFE_NO_PAD.encode(&bytes)
}
pub fn generate_session_id() -> String {
generate_token(32)
}
pub fn generate_csrf_token() -> String {
generate_token(32)
}
Constant-Time Comparison
use subtle::ConstantTimeEq;
pub fn secure_compare(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
a.ct_eq(b).into()
}
pub fn secure_compare_str(a: &str, b: &str) -> bool {
secure_compare(a.as_bytes(), b.as_bytes())
}
Secrets Management
Secure String Handling
use secrecy::{ExposeSecret, Secret};
use zeroize::Zeroize;
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()
}
}
#[derive(Zeroize)]
#[zeroize(drop)]
pub struct ApiKey {
key: String,
}
impl Drop for ApiKey {
fn drop(&mut self) {
self.zeroize();
}
}
Environment Variables
use secrecy::Secret;
use std::env;
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"))?
),
})
}
}
impl std::fmt::Debug {
(&, f: & std::fmt::Formatter<>) std::fmt:: {
f.()
.(, &)
.(, &)
.(, &)
.()
}
}
Cryptography
Use High-Level 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)?;
(in_out)
}
}
TLS Configuration
use rustls::{ClientConfig, RootCertStore};
use std::sync::Arc;
pub fn create_tls_config() -> Result<Arc<ClientConfig>, TlsError> {
let mut root_store = RootCertStore::empty();
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))
}
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
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();
timestamps.retain(|&t| t > cutoff);
timestamps.() >= .max_requests {
(RateLimitError::TooManyRequests);
}
timestamps.(now);
(())
}
}
Request Size Limits
use axum::{
body::Body,
extract::Request,
middleware::Next,
response::Response,
};
use http::StatusCode;
const MAX_BODY_SIZE: u64 = 1024 * 1024;
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
use tracing::{info, warn, error, instrument};
fn bad_login(username: &str, password: &str) {
info!("Login attempt: user={}, pass={}", username, password);
}
#[instrument(skip(password), fields(username = %username))]
fn good_login(username: &str, password: &str) -> Result<(), AuthError> {
info!("Login attempt");
Ok(())
}
#[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);
}
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error("Invalid credentials")]
InvalidCredentials,
#[error("Account locked")]
AccountLocked,
}
Security Headers
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();
headers.insert(
header::X_CONTENT_TYPE_OPTIONS,
"nosniff".parse().unwrap(),
);
headers.insert(
header::X_FRAME_OPTIONS,
"DENY".parse().unwrap(),
);
headers.insert(
header::STRICT_TRANSPORT_SECURITY,
"max-age=31536000; includeSubDomains".parse().unwrap(),
);
headers.insert(
header::CONTENT_SECURITY_POLICY,
"default-src 'self'".parse().unwrap(),
);
response
}
Security Checklist
Before Release
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 |