rust-security

Rust Security - Quick Reference

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "rust-security" with this command: npx skills add claude-dev-suite/claude-dev-suite/claude-dev-suite-claude-dev-suite-rust-security

Rust Security - Quick Reference

When NOT to Use This Skill

  • General OWASP concepts - Use owasp or owasp-top-10 skill

  • Java security - Use java-security skill

  • Python security - Use python-security skill

  • Secrets management - Use secrets-management skill

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: rust for Rust security documentation.

Rust's Built-in Security Advantages

Rust provides memory safety by default:

  • No null pointer dereferences (Option instead)

  • No buffer overflows (bounds checking)

  • No use-after-free (ownership system)

  • No data races (borrow checker)

However, Rust does NOT protect against:

  • Logic errors (authorization bugs)

  • SQL injection (string handling)

  • XSS (template handling)

  • Secrets exposure

  • Dependency vulnerabilities

Dependency Auditing

cargo-audit - Check for known vulnerabilities

cargo install cargo-audit cargo audit

cargo-deny - Policy-based linting

cargo install cargo-deny cargo deny check

Check outdated dependencies

cargo install cargo-outdated cargo outdated

Snyk for Rust

snyk test

cargo-deny Configuration (deny.toml)

[advisories] vulnerability = "deny" unmaintained = "warn" yanked = "deny"

[licenses] unlicensed = "deny" allow = ["MIT", "Apache-2.0", "BSD-3-Clause"]

[bans] multiple-versions = "warn" wildcards = "deny"

[sources] unknown-registry = "deny" unknown-git = "deny"

CI/CD Integration

GitHub Actions

  • name: Security audit run: | cargo install cargo-audit cargo audit

  • name: Dependency policy check run: | cargo install cargo-deny cargo deny check

SQL Injection Prevention

SQLx - Safe (Compile-time Checked)

use sqlx::{PgPool, query_as};

// SAFE - Compile-time verified query let user: Option<User> = sqlx::query_as!( User, "SELECT * FROM users WHERE email = $1", email ) .fetch_optional(&pool) .await?;

// SAFE - Runtime query with bind let user: Option<User> = sqlx::query_as::<_, User>( "SELECT * FROM users WHERE email = $1" ) .bind(&email) .fetch_optional(&pool) .await?;

Diesel - Safe (Type-safe ORM)

use diesel::prelude::*;

// SAFE - Type-safe query let user = users::table .filter(users::email.eq(&email)) .first::<User>(&mut conn) .optional()?;

// SAFE - Explicit parameter binding diesel::sql_query("SELECT * FROM users WHERE email = $1") .bind::<Text, _>(&email) .load::<User>(&mut conn)?;

UNSAFE Patterns

// UNSAFE - String formatting let query = format!("SELECT * FROM users WHERE email = '{}'", email); // NEVER!

// UNSAFE - String concatenation let query = "SELECT * FROM users WHERE email = '".to_owned() + &email + "'"; // NEVER!

XSS Prevention

Askama (Compile-time Templates - Auto-escaping)

use askama::Template;

#[derive(Template)] #[template(path = "page.html")] struct PageTemplate<'a> { user_input: &'a str, // Auto-escaped in template }

<!-- page.html - auto-escaped --> <p>{{ user_input }}</p>

<!-- Explicit raw (use with caution) --> <p>{{ user_input|safe }}</p> <!-- Only if already sanitized -->

Tera (Runtime Templates)

use tera::{Tera, Context};

let tera = Tera::new("templates/**/*")?; let mut ctx = Context::new(); ctx.insert("user_input", &user_input); // Auto-escaped

let rendered = tera.render("page.html", &ctx)?;

Manual Sanitization with ammonia

use ammonia::clean;

// Sanitize HTML input let safe_html = clean(&user_input);

// Custom policy use ammonia::Builder;

let safe_html = Builder::default() .tags(hashset!["p", "b", "i", "a"]) .url_schemes(hashset!["http", "https"]) .link_rel(Some("noopener noreferrer")) .clean(&user_input) .to_string();

Authentication - JWT

jsonwebtoken

use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey}; use serde::{Serialize, Deserialize}; use chrono::{Utc, Duration};

#[derive(Debug, Serialize, Deserialize)] struct Claims { sub: String, // user_id email: String, exp: usize, // expiration iat: usize, // issued at }

fn generate_token(user_id: &str, email: &str, secret: &[u8]) -> Result<String, Error> { let expiration = Utc::now() .checked_add_signed(Duration::hours(1)) .expect("valid timestamp") .timestamp() as usize;

let claims = Claims {
    sub: user_id.to_owned(),
    email: email.to_owned(),
    exp: expiration,
    iat: Utc::now().timestamp() as usize,
};

encode(
    &#x26;Header::new(Algorithm::HS256),
    &#x26;claims,
    &#x26;EncodingKey::from_secret(secret)
)

}

fn validate_token(token: &str, secret: &[u8]) -> Result<Claims, Error> { let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = true;

let token_data = decode::&#x3C;Claims>(
    token,
    &#x26;DecodingKey::from_secret(secret),
    &#x26;validation
)?;

Ok(token_data.claims)

}

Password Hashing with argon2

use argon2::{ password_hash::{ rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString }, Argon2 };

fn hash_password(password: &str) -> Result<String, Error> { let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default();

Ok(argon2
    .hash_password(password.as_bytes(), &#x26;salt)?
    .to_string())

}

fn verify_password(password: &str, hash: &str) -> Result<bool, Error> { let parsed_hash = PasswordHash::new(hash)?; Ok(Argon2::default() .verify_password(password.as_bytes(), &parsed_hash) .is_ok()) }

Input Validation with validator

use validator::{Validate, ValidationError}; use regex::Regex; use lazy_static::lazy_static;

lazy_static! { static ref NAME_REGEX: Regex = Regex::new(r"^[a-zA-Z\s-']+$").unwrap(); }

#[derive(Debug, Validate, Deserialize)] struct CreateUserRequest { #[validate(email, length(max = 255))] email: String,

#[validate(length(min = 12, max = 128), custom = "validate_password_strength")]
password: String,

#[validate(length(min = 2, max = 100), regex = "NAME_REGEX")]
name: String,

}

fn validate_password_strength(password: &str) -> Result<(), ValidationError> { let has_upper = password.chars().any(|c| c.is_uppercase()); let has_lower = password.chars().any(|c| c.is_lowercase()); let has_digit = password.chars().any(|c| c.is_numeric()); let has_special = password.chars().any(|c| "@$!%*?&".contains(c));

if has_upper &#x26;&#x26; has_lower &#x26;&#x26; has_digit &#x26;&#x26; has_special {
    Ok(())
} else {
    Err(ValidationError::new("password_strength"))
}

}

// Axum handler async fn create_user( Json(payload): Json<CreateUserRequest> ) -> Result<Json<User>, AppError> { payload.validate()?; // payload is validated }

Secure File Upload (Axum)

use axum::{ extract::Multipart, response::Json, }; use tokio::fs::File; use tokio::io::AsyncWriteExt; use uuid::Uuid;

const MAX_FILE_SIZE: usize = 10 * 1024 * 1024; // 10 MB const ALLOWED_TYPES: &[&str] = &["image/jpeg", "image/png", "application/pdf"];

async fn upload_file(mut multipart: Multipart) -> Result<Json<UploadResponse>, AppError> { while let Some(field) = multipart.next_field().await? { let content_type = field.content_type() .ok_or(AppError::BadRequest("Missing content type"))?;

    // Validate content type
    if !ALLOWED_TYPES.contains(&#x26;content_type) {
        return Err(AppError::BadRequest("File type not allowed"));
    }

    let data = field.bytes().await?;

    // Validate size
    if data.len() > MAX_FILE_SIZE {
        return Err(AppError::BadRequest("File too large"));
    }

    // Generate safe filename
    let ext = match content_type {
        "image/jpeg" => "jpg",
        "image/png" => "png",
        "application/pdf" => "pdf",
        _ => return Err(AppError::BadRequest("Unknown type")),
    };
    let safe_name = format!("{}.{}", Uuid::new_v4(), ext);

    // Save file
    let path = format!("uploads/{}", safe_name);
    let mut file = File::create(&#x26;path).await?;
    file.write_all(&#x26;data).await?;

    return Ok(Json(UploadResponse { filename: safe_name }));
}

Err(AppError::BadRequest("No file provided"))

}

CORS Configuration (Axum)

use tower_http::cors::{CorsLayer, Any}; use http::{HeaderValue, Method};

let cors = CorsLayer::new() .allow_origin("https://myapp.com".parse::&#x3C;HeaderValue>().unwrap()) .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE]) .allow_headers([http::header::AUTHORIZATION, http::header::CONTENT_TYPE]) .allow_credentials(true);

let app = Router::new() .route("/api/users", get(get_users)) .layer(cors);

Security Headers Middleware

use axum::{ middleware::{self, Next}, response::Response, http::Request, };

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("X-Content-Type-Options", "nosniff".parse().unwrap());
headers.insert("X-Frame-Options", "DENY".parse().unwrap());
headers.insert("X-XSS-Protection", "0".parse().unwrap());
headers.insert("Referrer-Policy", "strict-origin-when-cross-origin".parse().unwrap());
headers.insert("Content-Security-Policy", "default-src 'self'".parse().unwrap());
headers.insert(
    "Strict-Transport-Security",
    "max-age=31536000; includeSubDomains".parse().unwrap()
);

response

}

// Apply to router let app = Router::new() .route("/", get(index)) .layer(middleware::from_fn(security_headers));

Rate Limiting

use governor::{Quota, RateLimiter}; use nonzero_ext::nonzero; use std::sync::Arc;

// Create rate limiter let limiter = Arc::new(RateLimiter::direct( Quota::per_minute(nonzero!(10u32)) ));

// Middleware async fn rate_limit<B>( State(limiter): State<Arc<RateLimiter<...>>>, request: Request<B>, next: Next<B> ) -> Result<Response, StatusCode> { match limiter.check() { Ok() => Ok(next.run(request).await), Err() => Err(StatusCode::TOO_MANY_REQUESTS), } }

Secrets Management

use std::env;

#[derive(Clone)] struct Config { jwt_secret: String, database_url: String, api_key: String, }

impl Config { fn from_env() -> Result<Self, ConfigError> { Ok(Config { jwt_secret: env::var("JWT_SECRET") .map_err(|| ConfigError::Missing("JWT_SECRET"))?, database_url: env::var("DATABASE_URL") .map_err(|| ConfigError::Missing("DATABASE_URL"))?, api_key: env::var("API_KEY") .map_err(|_| ConfigError::Missing("API_KEY"))?, }) } }

// NEVER hardcode secrets // const JWT_SECRET: &str = "hardcoded-secret"; // NEVER!

Logging Security Events

use tracing::{info, warn};

fn log_login_attempt(username: &str, success: bool, ip: &str) { info!( user = username, success = success, ip = ip, "login attempt" ); }

fn log_access_denied(user_id: &str, resource: &str, ip: &str) { warn!( user_id = user_id, resource = resource, ip = ip, "access denied" ); }

// NEVER log sensitive data // info!(password = password, "user data"); // NEVER!

Unsafe Code Guidelines

// Minimize unsafe blocks // Document why unsafe is necessary // Encapsulate unsafe in safe abstractions

/// SAFETY: buffer is guaranteed to be valid UTF-8 /// because it was created from a valid String unsafe fn process_buffer(buffer: &[u8]) -> &str { std::str::from_utf8_unchecked(buffer) }

// Prefer safe alternatives let s = std::str::from_utf8(buffer)?; // Safe version

Anti-Patterns

Anti-Pattern Why It's Bad Correct Approach

format! in SQL query SQL injection Use query macros with bind

safe filter on user input XSS vulnerability

Hardcoded secrets Secret exposure Use environment variables

Excessive unsafe blocks Memory safety bypass Minimize and document unsafe

Ignoring cargo audit warnings Known vulnerabilities Update or replace dependencies

Weak JWT algorithms Token forgery Use HS256 minimum

unwrap() in handlers Panic in production Use proper error handling

Quick Troubleshooting

Issue Likely Cause Solution

cargo audit finds RUSTSEC Vulnerable crate Update with cargo update

JWT validation fails Wrong algorithm/key Check Algorithm enum and key

CORS error Origin not configured Add origin to CorsLayer

Password hash slow Argon2 params too high Adjust memory/iterations

SQLx compile error Query doesn't match schema Run cargo sqlx prepare

Template not escaping Using safe filter

Security Scanning Commands

Vulnerability audit

cargo audit

Policy check

cargo deny check

Clippy security lints

cargo clippy -- -W clippy::all -W clippy::pedantic

Check for secrets

gitleaks detect trufflehog git file://.

SAST with semgrep

semgrep --config=p/rust .

Related Skills

  • OWASP Top 10:2025

  • OWASP General

  • Secrets Management

  • Supply Chain Security

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Security

dotnet-security

No summary provided by upstream source.

Repository SourceNeeds Review
Security

spring-security

No summary provided by upstream source.

Repository SourceNeeds Review
Security

rust-security

No summary provided by upstream source.

Repository SourceNeeds Review
Security

go-security

No summary provided by upstream source.

Repository SourceNeeds Review