About rust-security
A Rust security reference skill for developers assessing and securing Rust applications. It combines dependency checks with guidance on common web risks, authentication, validation, and safe handling of unsafe code.
Key features
- 01Dependency auditing with cargo-audit
- 02Policy checks with cargo-deny
- 03SQL injection prevention patterns
- 04XSS and input validation guidance
- 05JWT and password hashing examples
- 06Secure Axum configuration
Use cases
- 01Auditing Rust dependencies in CI
- 02Reviewing database queries and templates
- 03Hardening authentication and file uploads
MCP support
The README points to mcp__documentation__fetch_docs for detailed Rust security documentation.
Install
Copy the command and run it in a terminal.
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill rust-securityAfter installing, type/rust-securityin Claude Code to confirm it loaded.
Details
From the project READMERust Security - Quick Reference
When NOT to Use This Skill
- General OWASP concepts - Use
owasporowasp-top-10skill - Java security - Use
java-securityskill - Python security - Use
python-securityskill - Secrets management - Use
secrets-managementskill
Deep Knowledge: Use
mcp__documentation__fetch_docswith technology:rustfor 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(
&Header::new(Algorithm::HS256),
&claims,
&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::<Claims>(
token,
&DecodingKey::from_secret(secret),
&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(), &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 && has_lower && has_digit && 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(&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(&path).await?;
file.write_all(&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::<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
Comments
Markdown supported