# Rust Web Framework Security Audit Guide

> Rust Web Framework Security Audit Module
> Applicable to: Actix-web 4.x, Axum 0.7+, Rocket 0.5+, sqlx, diesel, sea-orm

## Identification Patterns

```toml
# Cargo.toml identification
[dependencies]
actix-web = "4"
axum = "0.7"
rocket = "0.5"
sqlx = { version = "0.7", features = ["postgres"] }
diesel = { version = "2.1", features = ["postgres"] }

# Project structure (typical)
├── Cargo.toml
├── src/
│   ├── main.rs
│   ├── handlers/ (or routes/)
│   ├── models/
│   ├── db/
│   └── middleware/
└── migrations/
```

---

## SQL Injection Detection

```rust
// Dangerous: sqlx::query raw string concatenation
let q = format!("SELECT * FROM users WHERE name = '{}'", name);
sqlx::query(&q).fetch_all(&pool).await;  // ❌ Critical

// Dangerous: sqlx::query_as concatenation
let sql = format!("SELECT * FROM orders WHERE status = '{}' ORDER BY {}", status, sort);
sqlx::query_as::<_, Order>(&sql).fetch_all(&pool).await;  // ❌

// Dangerous: diesel raw_sql
diesel::sql_query(format!("SELECT * FROM users WHERE id = {}", id))
    .load::<User>(&mut conn);  // ❌

// Dangerous: sea-orm raw query
let stmt = Statement::from_string(
    DbBackend::Postgres,
    format!("SELECT * FROM users WHERE name = '{}'", input)  // ❌
);

// Audit regex (Rust)
format!\s*\(.*SELECT|format!\s*\(.*INSERT|format!\s*\(.*UPDATE|format!\s*\(.*DELETE
sql_query\s*\(format!|query\s*\(&format!|query_as.*&format!

// Safe: sqlx bound parameters
sqlx::query("SELECT * FROM users WHERE name = $1")
    .bind(&name)
    .fetch_all(&pool).await;  // ✓

// Safe: sqlx macro (compile-time verification)
sqlx::query!("SELECT * FROM users WHERE name = $1", name)
    .fetch_all(&pool).await;  // ✓ Compile-time type checking

// Safe: diesel query builder
users::table.filter(users::name.eq(&name)).load::<User>(&mut conn);  // ✓
```

---

## Path Traversal (File Serving)

```rust
// Dangerous: Actix NamedFile direct concatenation
async fn download(path: web::Path<String>) -> actix_files::NamedFile {
    let filepath = format!("./uploads/{}", path.into_inner());
    NamedFile::open(filepath).unwrap()  // ❌ ../../../etc/passwd
}

// Dangerous: Axum file read
async fn read_file(Path(name): Path<String>) -> impl IntoResponse {
    let content = tokio::fs::read_to_string(format!("./data/{}", name)).await;  // ❌
    content.unwrap_or_default()
}

// Dangerous: Rocket file path
#[get("/files/<path>")]
async fn files(path: &str) -> Option<NamedFile> {
    NamedFile::open(format!("static/{}", path)).await.ok()  // ❌
}

// Audit regex
NamedFile::open\s*\(format!|read_to_string\s*\(format!.*\{
tokio::fs::(read|write).*format!|std::fs::(read|write).*format!

// Safe: Path validation
async fn download(path: web::Path<String>) -> Result<NamedFile, Error> {
    let name = path.into_inner();
    if name.contains("..") || name.contains('/') || name.contains('\\') {
        return Err(actix_web::error::ErrorBadRequest("invalid path"));
    }
    let filepath = PathBuf::from("./uploads").join(&name);
    let canonical = filepath.canonicalize()?;
    if !canonical.starts_with(std::fs::canonicalize("./uploads")?) {  // ✓
        return Err(actix_web::error::ErrorForbidden("forbidden"));
    }
    Ok(NamedFile::open(canonical)?)
}

// Actix ServeDir safe (tower-http)
// ServeDir prevents path traversal by default, but check custom handling
```

---

## SSRF Detection

```rust
// Dangerous: reqwest with user-controlled URL
async fn fetch(url: web::Query<FetchParams>) -> impl Responder {
    let resp = reqwest::get(&url.target).await.unwrap();  // ❌ SSRF
    resp.text().await.unwrap()
}

// Dangerous: hyper client
let uri: Uri = user_input.parse().unwrap();
let resp = hyper_client.get(uri).await;  // ❌

// Audit regex
reqwest::(get|Client).*params|reqwest::get\s*\(&|\.get\s*\(&.*user
hyper::Uri.*parse\s*\(.*input|Client::new.*\.get\s*\(

// Safe: URL whitelist + internal IP check
use std::net::IpAddr;
use url::Url;

fn validate_url(input: &str) -> Result<Url, &'static str> {
    let url = Url::parse(input).map_err(|_| "invalid url")?;
    if url.scheme() != "https" { return Err("only https"); }

    let host = url.host_str().ok_or("no host")?;
    // DNS resolution to check IP
    let addr: IpAddr = host.parse()
        .or_else(|_| resolve_host(host))
        .map_err(|_| "resolve failed")?;

    if addr.is_loopback() || addr.is_private() || addr.is_link_local() {
        return Err("internal ip blocked");  // ✓
    }
    Ok(url)
}
```

---

## Unsafe Blocks and FFI Boundaries

```rust
// Audit focus: unsafe blocks
unsafe {
    let ptr = user_data.as_ptr();
    std::ptr::read(ptr.offset(offset as isize))  // ❌ Bounds not checked
}

// Dangerous: FFI calls without validation
extern "C" {
    fn external_process(data: *const u8, len: usize);
}
unsafe { external_process(input.as_ptr(), input.len()); }  // ❌ Trusts external data

// Dangerous: transmute type conversion
let value: u64 = unsafe { std::mem::transmute(user_bytes) };  // ❌

// Audit regex
unsafe\s*\{|std::mem::transmute|std::ptr::(read|write)|from_raw_parts
extern\s+"C"|#\[no_mangle\]

// Audit strategy: All unsafe blocks require manual review
// 1. Check bounds validation
// 2. Check null pointer handling
// 3. Check FFI input validation
// 4. Check lifetime correctness
```

---

## Race Conditions (Shared State)

```rust
// Dangerous: TOCTOU in Arc<Mutex>
async fn transfer(state: web::Data<Arc<Mutex<BankState>>>, req: TransferReq) {
    let mut s = state.lock().unwrap();
    if s.balance >= req.amount {
        drop(s);  // ❌ Lock released before operation
        // Other threads may modify balance here
        let mut s = state.lock().unwrap();
        s.balance -= req.amount;  // ❌ TOCTOU race condition
    }
}

// Dangerous: RwLock read-to-write upgrade race
let balance = state.read().unwrap().balance;  // Read
// Gap: Other threads may modify
if balance >= amount {
    state.write().unwrap().balance -= amount;  // ❌ Race condition
}

// Audit regex
Arc::new\s*\(Mutex::new|Arc::new\s*\(RwLock::new
\.lock\(\).*drop\s*\(|\.read\(\).*\.write\(\)

// Safe: Complete all operations while holding the lock
async fn transfer(state: web::Data<Arc<Mutex<BankState>>>, req: TransferReq) {
    let mut s = state.lock().unwrap();
    if s.balance >= req.amount {
        s.balance -= req.amount;  // ✓ Lock not released, atomic operation
    }
}

// Or use tokio::sync::Mutex for async
let mut guard = state.lock().await;  // ✓ Async lock
```

---

## JWT Handling (jsonwebtoken crate)

```rust
// Dangerous: Algorithm not verified
let token_data = decode::<Claims>(
    &token,
    &DecodingKey::from_secret(secret),
    &Validation::default()  // ⚠️ Allows multiple algorithms by default
)?;

// Dangerous: Expiration verification disabled
let mut validation = Validation::new(Algorithm::HS256);
validation.validate_exp = false;  // ❌ Does not verify expiration

// Dangerous: Weak secret
let key = DecodingKey::from_secret(b"secret");  // ❌ Weak secret

// Audit regex
validate_exp\s*=\s*false|validate_aud\s*=\s*false
from_secret\s*\(b"[^"]{1,16}"\)|DecodingKey.*"secret"

// Safe: Strict verification
let mut validation = Validation::new(Algorithm::HS256);  // ✓ Explicit algorithm
validation.set_audience(&["my-app"]);                    // ✓ Verify audience
validation.set_issuer(&["auth-server"]);                 // ✓ Verify issuer
// validate_exp defaults to true                          // ✓

let key = DecodingKey::from_secret(std::env::var("JWT_SECRET")?.as_bytes());  // ✓
let token_data = decode::<Claims>(&token, &key, &validation)?;
```

---

## CORS Configuration

```rust
// Dangerous: Actix CORS overly permissive
use actix_cors::Cors;
Cors::permissive()                    // ❌ Allows all
Cors::default()
    .allow_any_origin()               // ❌
    .allow_any_method()
    .supports_credentials()           // ❌ Conflicts with any_origin

// Audit regex
Cors::permissive|allow_any_origin|allowed_origin\(".*\*"\)

// Safe:
Cors::default()
    .allowed_origin("https://app.example.com")  // ✓
    .allowed_methods(vec!["GET", "POST"])
    .allowed_headers(vec!["Authorization", "Content-Type"])
    .supports_credentials()
    .max_age(3600)
```

---

## Input Validation (Missing Extractors)

```rust
// Dangerous: Unvalidated request body
async fn create_user(Json(body): Json<serde_json::Value>) -> impl IntoResponse {
    // ❌ Using generic Value, no type/range validation
    let name = body["name"].as_str().unwrap();
}

// Safe: Typed extractors + validator
#[derive(Deserialize, Validate)]
struct CreateUser {
    #[validate(length(min = 1, max = 100))]
    name: String,
    #[validate(email)]
    email: String,
}

async fn create_user(
    ValidatedJson(body): ValidatedJson<CreateUser>  // ✓ Auto-validated
) -> impl IntoResponse { ... }

// Audit regex
Json\(.*\):\s*Json<serde_json::Value>|Json<Value>
Path\(.*\):\s*Path<String>  // Path parameter without validation
```

---

## Search Pattern Summary

```regex
# SQL Injection
format!\s*\(.*SELECT|format!\s*\(.*INSERT|sql_query\s*\(format!

# Path Traversal
NamedFile::open\s*\(format!|tokio::fs::read.*format!

# SSRF
reqwest::(get|Client).*params|hyper::Uri.*parse.*input

# Unsafe
unsafe\s*\{|transmute|from_raw_parts

# Race Conditions
\.lock\(\).*drop|\.read\(\).*\.write\(\)

# JWT
validate_exp.*false|from_secret.*b"[^"]{1,16}"

# CORS
Cors::permissive|allow_any_origin

# Input Validation
Json<serde_json::Value>|Json<Value>
```

---

## Quick Audit Checklist

```markdown
[ ] Check Cargo.toml dependency versions (cargo audit)
[ ] Search for format! with SQL keywords (SQL injection)
[ ] Search for NamedFile::open/tokio::fs::read path concatenation (path traversal)
[ ] Search for reqwest/hyper user-controlled URLs (SSRF)
[ ] Review all unsafe blocks (memory safety)
[ ] Search for Arc<Mutex>/RwLock for race conditions
[ ] Search for jsonwebtoken configuration (JWT security)
[ ] Search for Cors::permissive / allow_any_origin (CORS)
[ ] Search for Json<Value> untyped input (missing validation)
[ ] Check .env / dotenv for hardcoded secrets
[ ] Search for unwrap()/expect() in request handlers (DoS/panic)
[ ] Check tower-http/actix-middleware security header configuration
```

---

## Minimum PoC Examples
```bash
# SQL Injection (format! concatenation)
curl "http://localhost:8080/users?name=admin'OR'1'='1"

# Path Traversal
curl "http://localhost:8080/files/../../../etc/passwd"

# SSRF
curl "http://localhost:8080/fetch?url=http://169.254.169.254/latest/meta-data/"
```

---

## Rust-Specific Security Advantages and Pitfalls

```markdown
Advantages:
- Memory safety (no buffer overflow / use-after-free) — in safe Rust
- Type system prevents many logic errors
- sqlx compile-time query checking

Pitfalls:
- unsafe blocks break all safety guarantees
- unwrap()/panic in handlers causes DoS
- format! SQL concatenation is still an injection vector
- Shared state race conditions (logic races, not memory races)
- FFI boundaries have no Rust safety guarantees
```

---

## Reference Resources

- [Rust Secure Coding Guidelines](https://anssi-fr.github.io/rust-guide/)
- [cargo-audit](https://github.com/rustsec/rustsec/tree/main/cargo-audit)
- [RustSec Advisory Database](https://rustsec.org/advisories/)
- [OWASP Rust Security](https://cheatsheetseries.owasp.org/cheatsheets/Rust_Cheat_Sheet.html)
