Segurança em Rust: Memory Safety Guia | Rust Brasil

Segurança em Rust: memory safety, type safety, thread safety e como Rust previne vulnerabilidades comuns.

Introdução

Rust é amplamente reconhecida por eliminar bugs de memória — use-after-free, buffer overflows, data races — em tempo de compilação. Porém, segurança de software vai muito além da segurança de memória. Aplicações Rust em produção enfrentam os mesmos desafios que qualquer outra linguagem: vulnerabilidades em dependências, ataques à cadeia de suprimentos, injeção de SQL, gerenciamento de segredos e validação inadequada de entrada.

Este artigo cobre as práticas de segurança que todo desenvolvedor Rust precisa conhecer para ir além do que o compilador garante automaticamente.

O Problema: Falsa Sensação de Segurança

O maior risco para desenvolvedores Rust é confiar que o compilador resolve todos os problemas de segurança. Aqui estão vulnerabilidades que o borrow checker NÃO detecta:

Não Faça Isso: Segredos Hardcoded

// ERRADO: Credenciais no código-fonte
const DATABASE_URL: &str = "postgres://admin:super_secreta_123@db.prod.exemplo.com/app";
const API_KEY: &str = "sk-live-abc123def456";

fn conectar_banco() -> Result<(), Box<dyn std::error::Error>> {
    // A senha vai parar no binário compilado e no histórico do git
    let _conn = DATABASE_URL; // simulação de conexão
    Ok(())
}

Não Faça Isso: Construção de SQL com Concatenação

// ERRADO: Vulnerável a SQL Injection
fn buscar_usuario_inseguro(nome: &str) -> String {
    // Um atacante pode passar: '; DROP TABLE usuarios; --
    format!("SELECT * FROM usuarios WHERE nome = '{nome}'")
}

fn main() {
    let input_malicioso = "'; DROP TABLE usuarios; --";
    let query = buscar_usuario_inseguro(input_malicioso);
    println!("Query gerada: {query}");
    // Query gerada: SELECT * FROM usuarios WHERE nome = ''; DROP TABLE usuarios; --'
}

Não Faça Isso: Input Não Validado

// ERRADO: Aceita qualquer input sem validação
fn criar_usuario(nome: &str, email: &str, idade: &str) -> String {
    let idade_num: i32 = idade.parse().unwrap_or(0);
    // Sem validação: nome pode ter 10MB, email pode não ser válido,
    // idade pode ser -1 ou 999
    format!("Usuário: {nome}, Email: {email}, Idade: {idade_num}")
}

A Solução: Segurança em Camadas

Camada 1: Auditoria de Dependências com cargo-audit

Instale e use cargo-audit para verificar se suas dependências têm vulnerabilidades conhecidas:

# Instalar
cargo install cargo-audit

# Verificar vulnerabilidades
cargo audit

# Saída típica:
# Crate:     chrono
# Version:   0.4.19
# Warning:   RUSTSEC-2020-0159
# Title:     Potential segfault in localtime_r invocations
# Solution:  Upgrade to >= 0.4.20

Adicione ao CI para verificação automática:

# .github/workflows/security.yml
name: Security Audit
on:
  push:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'  # Toda segunda-feira às 6h

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: rustsec/audit-check@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

Camada 2: Políticas de Dependência com cargo-deny

cargo-deny vai além do audit, verificando licenças, duplicações e fontes:

cargo install cargo-deny
cargo deny init  # Gera deny.toml
# deny.toml
[advisories]
vulnerability = "deny"
unmaintained = "warn"

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

[bans]
multiple-versions = "warn"
deny = [
    # Crates que não queremos no projeto
    { name = "openssl", wrappers = ["openssl-sys"] },
]

[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# Verificar todas as políticas
cargo deny check

# Verificar apenas licenças
cargo deny check licenses

Camada 3: Gerenciamento de Segredos

# Cargo.toml
[dependencies]
dotenvy = "0.15"
secrecy = "0.10"
use secrecy::{ExposeSecret, SecretString};
use std::env;

struct AppConfig {
    database_url: SecretString,
    api_key: SecretString,
    porta: u16,
}

impl AppConfig {
    fn from_env() -> Result<Self, env::VarError> {
        // Carrega .env apenas em desenvolvimento
        let _ = dotenvy::dotenv();

        Ok(AppConfig {
            database_url: SecretString::from(env::var("DATABASE_URL")?),
            api_key: SecretString::from(env::var("API_KEY")?),
            porta: env::var("PORT")
                .unwrap_or_else(|_| "8080".to_string())
                .parse()
                .unwrap_or(8080),
        })
    }
}

fn conectar(config: &AppConfig) {
    // SecretString não implementa Display/Debug — impede vazamento acidental em logs
    // println!("{}", config.database_url); // NÃO COMPILA

    // Para acessar o valor, é explícito:
    let url = config.database_url.expose_secret();
    println!("Conectando ao banco...");
    // Use `url` apenas onde necessário
    let _ = url;
}

fn main() {
    let config = AppConfig::from_env().expect("Variáveis de ambiente não configuradas");
    conectar(&config);
}

A crate secrecy garante que segredos:

  • Não apareçam em logs acidentalmente (sem Display/Debug)
  • São apagados da memória quando descartados (implementa Zeroize)
  • Requerem chamada explícita .expose_secret() para acesso

Camada 4: Validação de Input com Tipos

use std::fmt;

/// Email validado — só pode ser criado se o formato estiver correto
#[derive(Debug, Clone)]
pub struct Email(String);

impl Email {
    pub fn new(valor: &str) -> Result<Self, ValidationError> {
        let valor = valor.trim().to_lowercase();

        if valor.is_empty() {
            return Err(ValidationError::Campo("email está vazio".into()));
        }

        // Validação básica de formato
        let partes: Vec<&str> = valor.split('@').collect();
        if partes.len() != 2 || partes[0].is_empty() || !partes[1].contains('.') {
            return Err(ValidationError::Campo(
                format!("email inválido: {valor}")
            ));
        }

        if valor.len() > 254 {
            return Err(ValidationError::Campo("email muito longo".into()));
        }

        Ok(Email(valor))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for Email {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// Idade validada — garantida estar entre 0 e 150
#[derive(Debug, Clone, Copy)]
pub struct Idade(u8);

impl Idade {
    pub fn new(valor: u8) -> Result<Self, ValidationError> {
        if valor > 150 {
            return Err(ValidationError::Campo(
                format!("idade inválida: {valor}")
            ));
        }
        Ok(Idade(valor))
    }

    pub fn valor(&self) -> u8 {
        self.0
    }
}

/// Nome validado — não vazio, tamanho limitado, sem caracteres perigosos
#[derive(Debug, Clone)]
pub struct NomeUsuario(String);

impl NomeUsuario {
    pub fn new(valor: &str) -> Result<Self, ValidationError> {
        let valor = valor.trim();

        if valor.is_empty() {
            return Err(ValidationError::Campo("nome está vazio".into()));
        }

        if valor.len() > 100 {
            return Err(ValidationError::Campo("nome muito longo".into()));
        }

        // Rejeita caracteres perigosos
        if valor.contains(['<', '>', '\'', '"', ';', '\\']) {
            return Err(ValidationError::Campo(
                "nome contém caracteres não permitidos".into()
            ));
        }

        Ok(NomeUsuario(valor.to_string()))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

#[derive(Debug)]
pub enum ValidationError {
    Campo(String),
}

impl fmt::Display for ValidationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ValidationError::Campo(msg) => write!(f, "Erro de validação: {msg}"),
        }
    }
}

impl std::error::Error for ValidationError {}

// Agora é IMPOSSÍVEL criar um usuário com dados inválidos
struct UsuarioValidado {
    nome: NomeUsuario,
    email: Email,
    idade: Idade,
}

fn criar_usuario(
    nome: &str,
    email: &str,
    idade: u8,
) -> Result<UsuarioValidado, ValidationError> {
    Ok(UsuarioValidado {
        nome: NomeUsuario::new(nome)?,
        email: Email::new(email)?,
        idade: Idade::new(idade)?,
    })
}

Com essa abordagem, funções que recebem Email, NomeUsuario ou Idade têm a garantia em tempo de compilação de que os valores já foram validados.

Camada 5: Prevenção de SQL Injection com Queries Parametrizadas

# Cargo.toml
[dependencies]
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }
tokio = { version = "1", features = ["full"] }
use sqlx::PgPool;

struct Usuario {
    id: i64,
    nome: String,
    email: String,
}

// CORRETO: Query parametrizada — imune a SQL injection
async fn buscar_por_nome(pool: &PgPool, nome: &str) -> Result<Vec<Usuario>, sqlx::Error> {
    let usuarios = sqlx::query_as!(
        Usuario,
        r#"SELECT id, nome, email FROM usuarios WHERE nome = $1"#,
        nome  // Parâmetro seguro — nunca é interpolado na query
    )
    .fetch_all(pool)
    .await?;

    Ok(usuarios)
}

// CORRETO: Inserção segura com múltiplos parâmetros
async fn criar_usuario(
    pool: &PgPool,
    nome: &str,
    email: &str,
) -> Result<i64, sqlx::Error> {
    let resultado = sqlx::query_scalar!(
        r#"INSERT INTO usuarios (nome, email) VALUES ($1, $2) RETURNING id"#,
        nome,
        email
    )
    .fetch_one(pool)
    .await?;

    Ok(resultado)
}

O sqlx::query_as! verifica a query em tempo de compilação contra o banco de dados, garantindo que os tipos e colunas estejam corretos.

Guia Passo a Passo: Auditoria de Segurança

Passo 1: Configure Ferramentas

# Ferramentas essenciais de segurança
cargo install cargo-audit
cargo install cargo-deny
cargo install cargo-outdated

Passo 2: Crie um Checklist de Segurança

Execute estes comandos regularmente:

# 1. Verificar vulnerabilidades conhecidas
cargo audit

# 2. Verificar licenças e políticas
cargo deny check

# 3. Verificar dependências desatualizadas
cargo outdated

# 4. Verificar código unsafe
grep -rn "unsafe" src/

Passo 3: Revise Código unsafe

// ERRADO: unsafe desnecessário
unsafe fn somar(a: i32, b: i32) -> i32 {
    a + b // Nada aqui precisa de unsafe!
}

// CORRETO: unsafe apenas quando necessário, com justificativa documentada
/// # Safety
///
/// O ponteiro `dados` deve:
/// - Ser não-nulo e válido para leitura de `len` bytes
/// - Estar alinhado para o tipo `u8`
/// - Os dados não podem ser modificados durante a vida da slice
pub unsafe fn de_ponteiro_para_slice<'a>(dados: *const u8, len: usize) -> &'a [u8] {
    // SAFETY: O chamador garante que o ponteiro é válido conforme documentado
    std::slice::from_raw_parts(dados, len)
}

Passo 4: Proteja o .env e Segredos

# .gitignore — NUNCA commite esses arquivos
.env
.env.local
.env.production
*.pem
*.key
secrets/

Passo 5: Rate Limiting em APIs

# Cargo.toml
[dependencies]
tower = { version = "0.5", features = ["limit"] }
axum = "0.8"
tokio = { version = "1", features = ["full"] }
use axum::{routing::get, Router};
use tower::limit::RateLimitLayer;
use std::time::Duration;

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/api/dados", get(handler))
        .layer(RateLimitLayer::new(100, Duration::from_secs(60))); // 100 req/min

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn handler() -> &'static str {
    "OK"
}

Armadilhas Comuns

1. Confiar em HTTPS Sem Verificar Certificados

// ERRADO: Desabilita verificação de certificado
// Isso permite ataques man-in-the-middle
// reqwest::Client::builder()
//     .danger_accept_invalid_certs(true)
//     .build()?;

// CORRETO: Mantenha a verificação padrão
let client = reqwest::Client::new(); // Verifica certificados automaticamente

2. Logs com Informações Sensíveis

use secrecy::{ExposeSecret, SecretString};

struct LoginRequest {
    usuario: String,
    senha: SecretString,
}

// ERRADO: Logando dados sensíveis
fn autenticar_ruim(req: &LoginRequest) {
    println!("Tentativa de login: usuario={}, senha=***", req.usuario);
    // Em algum momento alguém vai "debugar" e trocar *** pela senha real
}

// CORRETO: Tipo SecretString impede exposição acidental
fn autenticar_bom(req: &LoginRequest) {
    println!("Tentativa de login: usuario={}", req.usuario);
    // req.senha não implementa Display, impossível logar acidentalmente
    let _senha = req.senha.expose_secret(); // Acesso explícito e rastreável
}

3. Deserialização Sem Limites

use serde::Deserialize;

#[derive(Deserialize)]
struct Payload {
    // ERRADO: Sem limite de tamanho — atacante pode enviar 10GB de dados
    // dados: Vec<u8>,

    // CORRETO: Valide limites após desserialização
    dados: Vec<u8>,
}

fn processar_payload(json: &str) -> Result<Payload, String> {
    let payload: Payload = serde_json::from_str(json)
        .map_err(|e| format!("JSON inválido: {e}"))?;

    // Valide o tamanho DEPOIS da desserialização
    if payload.dados.len() > 1_048_576 {
        return Err("Payload excede o limite de 1MB".into());
    }

    Ok(payload)
}

4. Timing Attacks em Comparação de Senhas

/// Comparação de tempo constante para evitar timing attacks
fn comparacao_segura(a: &[u8], b: &[u8]) -> bool {
    if a.len() != b.len() {
        return false;
    }

    let mut resultado: u8 = 0;
    for (x, y) in a.iter().zip(b.iter()) {
        resultado |= x ^ y;
    }
    resultado == 0
}

Exemplo do Mundo Real: Middleware de Segurança para Axum

use axum::{
    extract::Request,
    http::{header, HeaderValue, StatusCode},
    middleware::Next,
    response::Response,
};

async fn headers_seguranca(request: Request, next: Next) -> Response {
    let mut response = next.run(request).await;

    let headers = response.headers_mut();

    // Previne XSS
    headers.insert(
        header::X_CONTENT_TYPE_OPTIONS,
        HeaderValue::from_static("nosniff"),
    );

    // Previne clickjacking
    headers.insert(
        header::X_FRAME_OPTIONS,
        HeaderValue::from_static("DENY"),
    );

    // Força HTTPS
    headers.insert(
        header::STRICT_TRANSPORT_SECURITY,
        HeaderValue::from_static("max-age=31536000; includeSubDomains"),
    );

    // Content Security Policy
    headers.insert(
        header::CONTENT_SECURITY_POLICY,
        HeaderValue::from_static("default-src 'self'"),
    );

    response
}

Checklist de Segurança para Projetos Rust

  1. cargo audit no CI — bloqueie merges com vulnerabilidades
  2. cargo deny — controle licenças e fontes de dependências
  3. Variáveis de ambiente — nunca hardcode segredos
  4. secrecy — para valores sensíveis em memória
  5. Queries parametrizadas — nunca concatene SQL
  6. Validação com tipos — newtype pattern para dados validados
  7. Minimize unsafe — documente cada uso com # Safety
  8. Headers de segurança — CSP, HSTS, X-Frame-Options
  9. Rate limiting — proteja APIs contra abuso
  10. Atualize dependênciascargo outdated regularmente

Veja Também