Segurança e Auditoria — Fuzzing, Análise Estática e Hardening de Código Rust

Segurança e Auditoria — Fuzzing, Análise Estática e Hardening de Código Rust

Seguro em memória não quer dizer seguro: Rust não impede vazamento, impasse, estouro de inteiro em release nem erro de lógica. O texto trata do que a linguagem garante e do que não garante, e das ferramentas que cobrem o resto — fuzzing, Miri, análise estática e auditoria de dependências.
Rust

22 min de leitura

Rust elimina classes inteiras de vulnerabilidades que assolam C e C++ — buffer overflows, use-after-free, data races. Mas não elimina todas. Lógica de negócio incorreta, uso indevido de unsafe, deserialização de dados não confiáveis, inteiros que estouraram silenciosamente em release — estes problemas persistem independentemente da linguagem.

Segurança séria requer múltiplas camadas: o compilador como primeira defesa, ferramentas de análise estática, fuzzing para encontrar inputs que causam comportamento inesperado, auditoria de dependências, e práticas de código que minimizam a superfície de ataque. Este artigo cobre cada camada.

O que Rust já garante por padrão

// VULNERABILIDADES QUE RUST ELIMINA ESTATICAMENTE:

// 1. Buffer overflow — impossível em código seguro
let v = vec![1, 2, 3];
// let x = v[10]; // panic em debug E release — não corrupção silenciosa

// 2. Use-after-free — impossível
let s = String::from("hello");
let r = &s;
// drop(s); // erro de compilação: cannot move out of `s` — borrowed
// println!("{r}"); // referência inválida impossível

// 3. Data race — impossível
use std::sync::Arc;
// let dados = vec![1, 2, 3];
// Enviar referência mutável para duas threads: erro de compilação

// 4. Null pointer dereference — impossível (não há null em tipos seguros)
// let p: &i32 = std::ptr::null(); // isso é unsafe e requer declaração explícita

// O QUE RUST NÃO GARANTE:

// 1. Lógica de negócio correta
fn calcular_desconto(preco: f64, desconto: f64) -> f64 {
    preco - desconto  // bug: deveria ser preco * (1.0 - desconto / 100.0)
}

// 2. Overflow de inteiros em release (wraps silenciosamente)
fn somar(a: u8, b: u8) -> u8 {
    a + b  // 200u8 + 100u8 = 44 em release — não panic
}

// 3. Vulnerabilidades em código unsafe
unsafe fn ler_ptr(p: *const i32) -> i32 {
    *p  // pode ser null, dangling, desalinhado...
}

// 4. Deserialização de dados não confiáveis
// 5. Timing attacks em código criptográfico
// 6. Vulnerabilidades nas dependências

Fuzzing com cargo-fuzz

Fuzzing gera inputs aleatórios e mutados automaticamente, buscando crashes, panics e comportamento indefinido:

# Instala cargo-fuzz
cargo install cargo-fuzz

# Inicializa fuzzing no projeto
cargo fuzz init

# Cria um novo alvo de fuzzing
cargo fuzz add parsear_entrada

# Roda o fuzzer (LLVM libFuzzer)
cargo fuzz run parsear_entrada

# Com limites de tempo e tamanho
cargo fuzz run parsear_entrada -- -max_total_time=60 -max_len=1024

# Ver cobertura de código
cargo fuzz coverage parsear_entrada
// fuzz/fuzz_targets/parsear_entrada.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use minha_crate::{parsear_comando, processar_json, validar_url};

// Alvo 1: parser de comandos
fuzz_target!(|data: &[u8]| {
    // O fuzzer vai gerar bytes aleatórios
    // Queremos garantir que NENHUM input cause panic
    if let Ok(s) = std::str::from_utf8(data) {
        // Não deve panic — apenas retornar Ok ou Err
        let _ = parsear_comando(s);
    }
});

// fuzz/fuzz_targets/processar_json.rs
fuzz_target!(|data: &[u8]| {
    if let Ok(s) = std::str::from_utf8(data) {
        let _ = processar_json(s);
    }
});

// fuzz/fuzz_targets/parsear_url.rs
// Com tipos estruturados — o fuzzer gera structs diretamente
use arbitrary::Arbitrary;

#[derive(Debug, Arbitrary)]
struct EntradaUrl {
    esquema: String,
    host: String,
    porta: u16,
    caminho: String,
}

fuzz_target!(|entrada: EntradaUrl| {
    let url = format!(
        "{}://{}:{}/{}",
        entrada.esquema, entrada.host, entrada.porta, entrada.caminho
    );
    let _ = validar_url(&url);
});
// src/lib.rs — funções sendo fuzzeadas
use std::num::ParseIntError;

#[derive(Debug, PartialEq)]
pub enum ErroComando {
    Vazio,
    TokenInvalido(String),
    ArgumentoFaltando,
    NumeroInvalido(ParseIntError),
}

/// Parseia um comando no formato "CMD arg1 arg2 ..."
pub fn parsear_comando(entrada: &str) -> Result<Vec<String>, ErroComando> {
    if entrada.trim().is_empty() {
        return Err(ErroComando::Vazio);
    }

    let tokens: Vec<String> = entrada
        .split_whitespace()
        .map(|s| s.to_string())
        .collect();

    // Valida que nenhum token contém caracteres de controle
    for token in &tokens {
        if token.chars().any(|c| c.is_control()) {
            return Err(ErroComando::TokenInvalido(token.clone()));
        }
    }

    Ok(tokens)
}

pub fn processar_json(s: &str) -> Result<serde_json::Value, serde_json::Error> {
    serde_json::from_str(s)
}

pub fn validar_url(url: &str) -> bool {
    // Validação robusta que nunca deve panic
    if url.len() > 2048 { return false; }
    url.starts_with("http://") || url.starts_with("https://")
}

Property-based testing com proptest

Enquanto fuzzing gera bytes aleatórios, proptest gera dados estruturados seguindo propriedades lógicas:

[dev-dependencies]
proptest = "1"
proptest-derive = "0.4"
use proptest::prelude::*;

// Função a ser testada
fn ordenar_e_deduplicar(mut v: Vec<i32>) -> Vec<i32> {
    v.sort();
    v.dedup();
    v
}

fn comprimir(dados: &[u8]) -> Vec<u8> {
    // implementação real usaria flate2 ou similar
    dados.to_vec() // placeholder
}

fn descomprimir(dados: &[u8]) -> Vec<u8> {
    dados.to_vec() // placeholder
}

proptest! {
    // Propriedade 1: ordenar e deduplicar produz resultado ordenado
    #[test]
    fn resultado_sempre_ordenado(v in prop::collection::vec(any::<i32>(), 0..100)) {
        let resultado = ordenar_e_deduplicar(v);
        for janela in resultado.windows(2) {
            prop_assert!(janela[0] <= janela[1],
                "Não está ordenado: {} > {}", janela[0], janela[1]);
        }
    }

    // Propriedade 2: sem duplicatas
    #[test]
    fn sem_duplicatas(v in prop::collection::vec(any::<i32>(), 0..100)) {
        let resultado = ordenar_e_deduplicar(v);
        for janela in resultado.windows(2) {
            prop_assert_ne!(janela[0], janela[1],
                "Duplicata encontrada: {}", janela[0]);
        }
    }

    // Propriedade 3: compressão/descompressão é reversível
    #[test]
    fn compressao_reversivel(dados in prop::collection::vec(any::<u8>(), 0..1024)) {
        let comprimido = comprimir(&dados);
        let descomprimido = descomprimir(&comprimido);
        prop_assert_eq!(dados, descomprimido);
    }

    // Propriedade 4: parse e serialização são inversas
    #[test]
    fn parse_serializar_inverso(
        n in any::<i64>(),
        s in "[a-zA-Z0-9_]{1,20}",
    ) {
        // Se serializa e parseia de volta, deve ser idêntico
        let serializado = format!("{n}:{s}");
        let partes: Vec<&str> = serializado.splitn(2, ':').collect();
        prop_assert_eq!(partes.len(), 2);
        prop_assert_eq!(partes[0].parse::<i64>().unwrap(), n);
        prop_assert_eq!(partes[1], s);
    }

    // Propriedade 5: operações monetárias preservam total
    #[test]
    fn transferencia_preserva_total(
        saldo_a in 0u64..1_000_000,
        saldo_b in 0u64..1_000_000,
        valor in 0u64..1_000_000,
    ) {
        let total_antes = saldo_a + saldo_b;

        let (novo_a, novo_b) = if valor <= saldo_a {
            (saldo_a - valor, saldo_b + valor)
        } else {
            (saldo_a, saldo_b) // transferência rejeitada
        };

        prop_assert_eq!(novo_a + novo_b, total_antes,
            "Total não preservado após transferência");
    }
}

// Geradores customizados
fn email_valido() -> impl Strategy<Value = String> {
    (
        "[a-z]{1,10}",    // usuário
        "[a-z]{1,10}",    // domínio
        "(com|org|net)",  // TLD
    ).prop_map(|(u, d, t)| format!("{u}@{d}.{t}"))
}

fn cpf_gerado() -> impl Strategy<Value = String> {
    // Gera CPF no formato correto (sem validação de dígitos)
    (
        prop::array::uniform3(0u8..10),  // 3 primeiros dígitos
        prop::array::uniform3(0u8..10),  // 3 seguintes
        prop::array::uniform3(0u8..10),  // 3 seguintes
        prop::array::uniform2(0u8..10),  // 2 verificadores
    ).prop_map(|(a, b, c, d)| {
        format!("{}{}{}.{}{}{}.{}{}{}-{}{}",
            a[0], a[1], a[2],
            b[0], b[1], b[2],
            c[0], c[1], c[2],
            d[0], d[1])
    })
}

proptest! {
    #[test]
    fn validar_email_nao_panics(email in email_valido()) {
        // Deve aceitar todos os emails gerados
        let valido = email.contains('@') && email.contains('.');
        prop_assert!(valido);
    }
}

Análise estática — além do compilador

# Clippy — linter oficial com centenas de verificações
cargo clippy
cargo clippy -- -D warnings          # trata avisos como erros
cargo clippy --all-features -- 
    -W clippy::pedantic             # verificações mais rigorosas
    -W clippy::nursery              # verificações experimentais
    -W clippy::cargo                 # verificações do Cargo.toml

# Auditoria de vulnerabilidades em dependências
cargo install cargo-audit
cargo audit

# Atualização de dependências com vulnerabilidades conhecidas
cargo audit fix

# Verifica dependências desatualizadas
cargo install cargo-outdated
cargo outdated

# Analisa tamanho binário e identifica código inflado
cargo install cargo-bloat
cargo bloat --release
cargo bloat --release --crates       # por crate

# Verifica licenças das dependências
cargo install cargo-license
cargo license

# Miri — interpretador que detecta UB em unsafe
cargo +nightly miri test
cargo +nightly miri run

# Sanitizers — detectam problemas em runtime
RUSTFLAGS="-Z sanitizer=address" cargo +nightly test
RUSTFLAGS="-Z sanitizer=thread"  cargo +nightly test
RUSTFLAGS="-Z sanitizer=memory"  cargo +nightly test
RUSTFLAGS="-Z sanitizer=leak"    cargo +nightly test

Detectando comportamento indefinido com Miri

// Este código tem UB — Miri detecta
pub fn ub_desalinhamento() {
    let dados = [0u8; 4];
    let ptr = dados.as_ptr() as *const u32;

    unsafe {
        // UB se ptr não está alinhado para u32
        // Miri reporta: "attempted to create a misaligned reference"
        let _valor = ptr.read_unaligned(); // correto
        let _valor_ub = *ptr;             // potencial UB
    }
}

// Miri detecta: leitura de memória não inicializada
pub fn memoria_nao_inicializada() {
    let mut x: u32 = unsafe {
        std::mem::MaybeUninit::uninit().assume_init() // UB!
    };
    // Miri: "using uninitialized data"
}

// Versão correta
pub fn memoria_inicializada() -> u32 {
    let x = std::mem::MaybeUninit::<u32>::new(42);
    unsafe { x.assume_init() } // ok — foi inicializado
}

// Detecta: aliasing mutável
pub fn aliasing_mutavel() {
    let mut x = 5i32;
    let r1 = &mut x as *mut i32;
    let r2 = &mut x as *mut i32;

    unsafe {
        *r1 = 10;
        *r2 = 20; // UB — dois ponteiros mutáveis para o mesmo local
    }
}

Overflow de inteiros — auditoria e prevenção

// O comportamento padrão em Rust:
// DEBUG:   panic em overflow (u8: 255 + 1 = panic)
// RELEASE: wrapping silencioso (u8: 255 + 1 = 0)

// Estratégia 1: checked — retorna None em overflow
fn soma_segura(a: u32, b: u32) -> Option<u32> {
    a.checked_add(b)
}

fn calcular_total(precos: &[u32]) -> Option<u32> {
    precos.iter().try_fold(0u32, |acc, &p| acc.checked_add(p))
}

// Estratégia 2: saturating — clipa ao máximo/mínimo
fn aumentar_nivel(nivel: u8, bonus: u8) -> u8 {
    nivel.saturating_add(bonus) // nunca ultrapassa 255
}

// Estratégia 3: wrapping — comportamento explícito
fn hash_simples(bytes: &[u8]) -> u32 {
    bytes.iter().fold(0u32, |acc, &b| {
        acc.wrapping_mul(31).wrapping_add(b as u32)
    })
}

// Estratégia 4: overflowing — retorna valor e flag
fn somar_com_flag(a: u32, b: u32) -> (u32, bool) {
    a.overflowing_add(b)
}

// Estratégia 5: tipos que sempre verificam (via newtype)
#[derive(Debug, Clone, Copy)]
struct DinheiroEmCentavos(u64);

impl DinheiroEmCentavos {
    fn novo(centavos: u64) -> Self {
        DinheiroEmCentavos(centavos)
    }

    fn somar(self, outro: Self) -> Option<Self> {
        self.0.checked_add(outro.0).map(DinheiroEmCentavos)
    }

    fn subtrair(self, outro: Self) -> Option<Self> {
        self.0.checked_sub(outro.0).map(DinheiroEmCentavos)
    }

    fn multiplicar(self, fator: u64) -> Option<Self> {
        self.0.checked_mul(fator).map(DinheiroEmCentavos)
    }

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

// Ativa overflow checks em release também
// Cargo.toml:
// [profile.release]
// overflow-checks = true

#[cfg(test)]
mod testes_overflow {
    use super::*;

    #[test]
    fn soma_segura_sem_overflow() {
        assert_eq!(soma_segura(100, 200), Some(300));
        assert_eq!(soma_segura(u32::MAX, 1), None);
    }

    #[test]
    fn dinheiro_nao_overflow() {
        let a = DinheiroEmCentavos::novo(u64::MAX);
        let b = DinheiroEmCentavos::novo(1);
        assert!(a.somar(b).is_none());
    }

    #[test]
    fn saturating_nunca_wrap() {
        assert_eq!(255u8.saturating_add(100), 255);
        assert_eq!(0u8.saturating_sub(1), 0);
    }
}

Uso seguro de unsafe

use std::alloc::{alloc, dealloc, Layout};

// Regras para código unsafe correto:
// 1. Documente os invariantes de segurança
// 2. Minimize o escopo do bloco unsafe
// 3. Use abstrações seguras na interface pública
// 4. Teste exaustivamente com Miri e sanitizers

/// Buffer de tamanho fixo com layout de memória controlado.
///
/// # Invariantes de segurança
///
/// - `ptr` é sempre válido e alinhado para `T`
/// - `ptr` aponta para `capacidade` elementos inicializados
/// - `len <= capacidade` em todo momento
pub struct BufferFixo<T> {
    ptr: *mut T,
    len: usize,
    capacidade: usize,
}

impl<T: Clone + Default> BufferFixo<T> {
    /// Aloca um buffer de `capacidade` elementos.
    ///
    /// # Panics
    ///
    /// Panics se `capacidade == 0` ou se a alocação falhar.
    pub fn novo(capacidade: usize) -> Self {
        assert!(capacidade > 0, "Capacidade não pode ser zero");

        let layout = Layout::array::<T>(capacidade)
            .expect("Layout inválido");

        // SAFETY: layout é válido e não-zero (assert acima garante)
        let ptr = unsafe { alloc(layout) as *mut T };

        if ptr.is_null() {
            panic!("Falha na alocação de memória");
        }

        // Inicializa todos os elementos com o valor padrão
        // SAFETY: ptr é válido, alinhado, e temos `capacidade` slots
        unsafe {
            for i in 0..capacidade {
                ptr.add(i).write(T::default());
            }
        }

        BufferFixo { ptr, len: 0, capacidade }
    }

    /// Retorna o número de elementos no buffer.
    pub fn len(&self) -> usize { self.len }

    /// Retorna true se o buffer estiver vazio.
    pub fn is_empty(&self) -> bool { self.len == 0 }

    /// Adiciona um elemento ao final.
    ///
    /// # Errors
    ///
    /// Retorna `Err` se o buffer estiver cheio.
    pub fn push(&mut self, valor: T) -> Result<(), T> {
        if self.len >= self.capacidade {
            return Err(valor);
        }

        // SAFETY: self.len < self.capacidade, então ptr.add(self.len)
        // está dentro dos limites alocados
        unsafe {
            self.ptr.add(self.len).write(valor);
        }

        self.len += 1;
        Ok(())
    }

    /// Remove e retorna o último elemento.
    pub fn pop(&mut self) -> Option<T> {
        if self.len == 0 {
            return None;
        }

        self.len -= 1;

        // SAFETY: self.len foi decrementado, o elemento em self.len
        // está inicializado (invariante mantido por push)
        let valor = unsafe { self.ptr.add(self.len).read() };
        Some(valor)
    }

    /// Acesso indexado com verificação de limites.
    pub fn obter(&self, indice: usize) -> Option<&T> {
        if indice >= self.len {
            return None;
        }

        // SAFETY: indice < self.len <= self.capacidade,
        // então está dentro dos limites e inicializado
        Some(unsafe { &*self.ptr.add(indice) })
    }
}

impl<T> Drop for BufferFixo<T> {
    fn drop(&mut self) {
        // SAFETY: ptr foi alocado com este mesmo layout em `novo()`
        // e não foi desalocado antes (Rust garante que Drop é chamado
        // no máximo uma vez)
        unsafe {
            // Dropa cada elemento para liberar recursos
            for i in 0..self.len {
                self.ptr.add(i).drop_in_place();
            }

            let layout = Layout::array::<T>(self.capacidade).unwrap();
            dealloc(self.ptr as *mut u8, layout);
        }
    }
}

// SAFETY: BufferFixo<T> pode ser enviado entre threads se T: Send
// O acesso ao ponteiro é protegido pelo borrow checker através da
// interface pública (sem &mut -> sem Send simultâneo)
unsafe impl<T: Send> Send for BufferFixo<T> {}

#[cfg(test)]
mod testes_buffer {
    use super::*;

    #[test]
    fn operacoes_basicas() {
        let mut buf: BufferFixo<i32> = BufferFixo::novo(3);

        assert!(buf.is_empty());
        assert_eq!(buf.push(10), Ok(()));
        assert_eq!(buf.push(20), Ok(()));
        assert_eq!(buf.push(30), Ok(()));
        assert_eq!(buf.push(40), Err(40)); // cheio

        assert_eq!(buf.len(), 3);
        assert_eq!(buf.obter(0), Some(&10));
        assert_eq!(buf.obter(1), Some(&20));
        assert_eq!(buf.obter(3), None); // fora dos limites

        assert_eq!(buf.pop(), Some(30));
        assert_eq!(buf.pop(), Some(20));
        assert_eq!(buf.pop(), Some(10));
        assert_eq!(buf.pop(), None);
    }
}

Validação de dados não confiáveis

use std::collections::HashSet;

#[derive(Debug, thiserror::Error)]
pub enum ErroValidacao {
    #[error("Campo obrigatório ausente: {0}")]
    CampoAusente(&'static str),

    #[error("Valor fora do intervalo permitido: {campo} = {valor} (min: {min}, max: {max})")]
    ForaDoIntervalo {
        campo: &'static str,
        valor: i64,
        min: i64,
        max: i64,
    },

    #[error("String inválida: {0}")]
    StringInvalida(String),

    #[error("Email inválido: {0}")]
    EmailInvalido(String),

    #[error("Tamanho excedido: {campo} tem {tamanho} bytes (max: {max})")]
    TamanhoExcedido {
        campo: &'static str,
        tamanho: usize,
        max: usize,
    },

    #[error("Caracteres não permitidos em: {0}")]
    CaracteresNaoPermitidos(String),
}

/// Sanitiza e valida uma string de input do usuário.
///
/// Garante que:
/// - Não excede comprimento máximo
/// - Não contém caracteres de controle (exceto 
, 	)
/// - Não contém null bytes
/// - Trim de espaços externos
pub fn sanitizar_string(
    entrada: &str,
    campo: &'static str,
    max_len: usize,
) -> Result<String, ErroValidacao> {
    // Remove espaços externos
    let entrada = entrada.trim();

    // Verifica tamanho (em bytes, não caracteres)
    if entrada.len() > max_len {
        return Err(ErroValidacao::TamanhoExcedido {
            campo,
            tamanho: entrada.len(),
            max: max_len,
        });
    }

    // Verifica caracteres inválidos
    if entrada.chars().any(|c| {
        c == '\0' ||                          // null byte
        (c.is_control() && c != '\n' && c != '\t' && c != '\r')
    }) {
        return Err(ErroValidacao::CaracteresNaoPermitidos(campo.to_string()));
    }

    Ok(entrada.to_string())
}

/// Valida email com verificações básicas.
/// Não é RFC 5321 completo — use crate `email_address` para produção.
pub fn validar_email(email: &str) -> Result<String, ErroValidacao> {
    let email = sanitizar_string(email, "email", 254)?;

    let partes: Vec<&str> = email.splitn(2, '@').collect();
    if partes.len() != 2 {
        return Err(ErroValidacao::EmailInvalido(email));
    }

    let (usuario, dominio) = (partes[0], partes[1]);

    if usuario.is_empty() || usuario.len() > 64 {
        return Err(ErroValidacao::EmailInvalido(email));
    }

    if !dominio.contains('.') || dominio.starts_with('.') {
        return Err(ErroValidacao::EmailInvalido(email));
    }

    // Apenas caracteres permitidos
    let permitidos_usuario: HashSet<char> =
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._%+-"
        .chars().collect();

    if !usuario.chars().all(|c| permitidos_usuario.contains(&c)) {
        return Err(ErroValidacao::EmailInvalido(email));
    }

    Ok(email.to_lowercase())
}

/// Valida dados de uma requisição HTTP desconfiável.
#[derive(Debug, serde::Deserialize)]
pub struct RequisicaoBruta {
    pub nome: Option<String>,
    pub email: Option<String>,
    pub idade: Option<i64>,
    pub mensagem: Option<String>,
}

#[derive(Debug)]
pub struct RequisicaoValidada {
    pub nome: String,
    pub email: String,
    pub idade: u8,
    pub mensagem: Option<String>,
}

pub fn validar_requisicao(
    bruta: RequisicaoBruta,
) -> Result<RequisicaoValidada, Vec<ErroValidacao>> {
    let mut erros = Vec::new();
    let mut resultado = RequisicaoValidada {
        nome: String::new(),
        email: String::new(),
        idade: 0,
        mensagem: None,
    };

    // Valida nome
    match bruta.nome {
        None => erros.push(ErroValidacao::CampoAusente("nome")),
        Some(n) => match sanitizar_string(&n, "nome", 100) {
            Ok(nome) if nome.is_empty() =>
                erros.push(ErroValidacao::CampoAusente("nome")),
            Ok(nome) => resultado.nome = nome,
            Err(e) => erros.push(e),
        },
    }

    // Valida email
    match bruta.email {
        None => erros.push(ErroValidacao::CampoAusente("email")),
        Some(e) => match validar_email(&e) {
            Ok(email) => resultado.email = email,
            Err(err)  => erros.push(err),
        },
    }

    // Valida idade
    match bruta.idade {
        None => erros.push(ErroValidacao::CampoAusente("idade")),
        Some(idade) => {
            if !(0..=150).contains(&idade) {
                erros.push(ErroValidacao::ForaDoIntervalo {
                    campo: "idade",
                    valor: idade,
                    min: 0,
                    max: 150,
                });
            } else {
                resultado.idade = idade as u8;
            }
        }
    }

    // Mensagem é opcional
    if let Some(msg) = bruta.mensagem {
        match sanitizar_string(&msg, "mensagem", 1000) {
            Ok(m) if !m.is_empty() => resultado.mensagem = Some(m),
            Ok(_) => {},
            Err(e) => erros.push(e),
        }
    }

    if erros.is_empty() {
        Ok(resultado)
    } else {
        Err(erros)
    }
}

Hardening de binários e configurações de segurança

# Cargo.toml — configurações de segurança para release

[profile.release]
# Overflow checks mesmo em release
overflow-checks = true

# Remove símbolos de debug (reduz superfície de ataque)
strip = "symbols"

# Link-time optimization — reduz código morto
lto = "fat"

# Controle de fluxo garantido pelo compilador
# (mitigação para ROP/JOP attacks em x86_64)
# RUSTFLAGS = "-C control-flow-guard"  # Windows
# RUSTFLAGS = "-C cf-protection=full"  # Linux com Intel CET

[profile.release.build-override]
opt-level = 3
// Configurações de segurança no código

// 1. Habilita todos os warnings relevantes
#![warn(
    missing_docs,
    clippy::all,
    clippy::pedantic,
    clippy::nursery,
    clippy::cargo,
    // Segurança específica:
    clippy::unwrap_used,        // use expect() com mensagem
    clippy::expect_used,        // considere ? em vez de expect
    clippy::panic,              // evite panic! em código de biblioteca
    clippy::indexing_slicing,   // use .get() em vez de []
    clippy::integer_arithmetic, // use checked/saturating
    clippy::as_conversions,     // use TryFrom em vez de as
)]

// 2. Zera memória sensível ao descartar
use std::ops::Drop;

struct ChaveSecreta {
    dados: Vec<u8>,
}

impl Drop for ChaveSecreta {
    fn drop(&mut self) {
        // Garante zeragem antes de liberar memória
        // Evita que chaves fiquem em swap ou core dumps
        for byte in &mut self.dados {
            // Escrita volátil — o compilador não pode otimizar fora
            unsafe {
                std::ptr::write_volatile(byte as *mut u8, 0);
            }
        }
    }
}

// Para produção: use a crate `zeroize`
// use zeroize::Zeroize;
// #[derive(Zeroize)]
// #[zeroize(drop)]
// struct ChaveSecreta { dados: Vec<u8> }

// 3. Limites no uso de recursos (proteção contra DoS)
fn processar_com_limite(
    dados: &[u8],
    max_bytes: usize,
    max_tempo: std::time::Duration,
) -> Result<Vec<u8>, String> {
    if dados.len() > max_bytes {
        return Err(format!(
            "Dados excedem limite: {} > {} bytes",
            dados.len(), max_bytes
        ));
    }

    let inicio = std::time::Instant::now();

    // Processamento com verificação periódica de tempo
    let mut resultado = Vec::with_capacity(dados.len());
    for (i, &byte) in dados.iter().enumerate() {
        // Verifica timeout a cada 10k bytes
        if i % 10_000 == 0 && inicio.elapsed() > max_tempo {
            return Err("Timeout excedido".to_string());
        }
        resultado.push(byte ^ 0x42); // operação fictícia
    }

    Ok(resultado)
}

// 4. Constante-time comparison para dados sensíveis
// Evita timing attacks em comparações de tokens/senhas
fn comparar_constante_time(a: &[u8], b: &[u8]) -> bool {
    if a.len() != b.len() {
        return false;
    }

    // Compara todos os bytes sem short-circuit
    // O compilador não pode otimizar isso para parar cedo
    let mut diferenca = 0u8;
    for (&byte_a, &byte_b) in a.iter().zip(b.iter()) {
        diferenca |= byte_a ^ byte_b;
    }

    diferenca == 0
    // Na prática use: subtle::ConstantTimeEq ou ring::constant_time
}

#[cfg(test)]
mod testes_seguranca {
    use super::*;

    #[test]
    fn validacao_string_null_byte() {
        let com_null = "teste\0injetado";
        let resultado = sanitizar_string(com_null, "campo", 100);
        assert!(resultado.is_err());
    }

    #[test]
    fn validacao_string_muito_longa() {
        let longa = "a".repeat(10001);
        let resultado = sanitizar_string(&longa, "campo", 100);
        assert!(matches!(resultado, Err(ErroValidacao::TamanhoExcedido { .. })));
    }

    #[test]
    fn email_invalido_rejeitado() {
        for email in ["nao-e-email", "@sem-usuario", "sem-dominio@", "a@b"] {
            assert!(
                validar_email(email).is_err(),
                "Deveria rejeitar: {email}"
            );
        }
    }

    #[test]
    fn email_valido_aceito() {
        for email in ["user@example.com", "nome.sobrenome@empresa.com.br"] {
            assert!(
                validar_email(email).is_ok(),
                "Deveria aceitar: {email}"
            );
        }
    }

    #[test]
    fn comparacao_constante_time_correta() {
        assert!(comparar_constante_time(b"senha123", b"senha123"));
        assert!(!comparar_constante_time(b"senha123", b"senha124"));
        assert!(!comparar_constante_time(b"curto", b"mais_longo"));
    }

    #[test]
    fn requisicao_com_multiplos_erros() {
        let bruta = RequisicaoBruta {
            nome: None,
            email: Some("email-invalido".to_string()),
            idade: Some(999),
            mensagem: None,
        };

        let resultado = validar_requisicao(bruta);
        assert!(resultado.is_err());
        let erros = resultado.unwrap_err();
        // Deve coletar TODOS os erros, não apenas o primeiro
        assert!(erros.len() >= 2);
    }
}

Workflow de auditoria de segurança

# Checklist completo de segurança para um projeto Rust

# 1. Compilação limpa sem warnings
cargo build --all-features 2>&1 | grep -c warning
# deve ser 0

# 2. Clippy sem problemas
cargo clippy --all-features -- -D warnings

# 3. Auditoria de dependências
cargo audit

# 4. Verificação de licenças
cargo license | grep -v "MIT|Apache|BSD|ISC"
# qualquer resultado requer atenção legal

# 5. Dependências não usadas
cargo install cargo-machete
cargo machete

# 6. Cobertura de testes
cargo install cargo-tarpaulin
cargo tarpaulin --all-features --out html
# objetivo: >80% de cobertura

# 7. Testes de overflow em debug
cargo test  # overflow em debug sempre causa panic

# 8. Miri para código unsafe
cargo +nightly miri test

# 9. Fuzzing (mínimo 1 hora por alvo exposto a dados externos)
cargo fuzz run parsear_entrada -- -max_total_time=3600

# 10. Sanitizers
RUSTFLAGS="-Z sanitizer=address" 
    cargo +nightly test --target x86_64-unknown-linux-gnu

# 11. Benchmarks de segurança (timing attacks)
cargo bench -- timing

# 12. Verificação de binário hardened (Linux)
checksec --file=target/release/meu_programa
# deve ter: RELRO Full, Stack Canary, NX, PIE

Exercícios

Exercício 1

O que Rust garante por padrão, e o que ele explicitamente não garante?

Ver resposta

✓ Resposta: Garante ausência de acesso inválido à memória em código seguro — sem uso depois da liberação, sem liberação dupla, sem ponteiro pendurado, sem corrida de dados. Não garante ausência de: vazamento de memória (ciclo de Rc vaza e isso é considerado seguro), impasse entre cadeados, estouro de inteiro em modo release, pânico por índice inválido, nem qualquer erro de lógica. Também não garante nada dentro de unsafe nem além da fronteira de FFI. Confundir "seguro em memória" com "seguro" é o equívoco mais caro do assunto.

Exercício 2

Por que o comportamento do estouro de inteiro muda entre debug e release, e o que fazer?

Ver resposta

✓ Resposta: Em debug o estouro causa pânico; em release, por padrão, ele dá a volta silenciosamente — a escolha foi por desempenho. A consequência é um bug que os testes não pegam e a produção exibe. Onde o valor vem de fonte não confiável, a defesa é explícita: checked_add devolve Option, saturating_add trava no limite, wrapping_add declara que a volta é intencional. É possível ainda ligar overflow-checks = true no perfil de release, pagando o custo em troca da verificação.

Exercício 3

O que é fuzzing e que tipo de bug ele encontra que o teste comum não encontra?

Ver resposta

✓ Resposta: É gerar entradas automaticamente — aleatórias e guiadas por cobertura — e executar a função até ela falhar. Encontra o que ninguém pensou em escrever: a entrada vazia, o byte inesperado no meio, o valor no limite exato, a sequência que leva a um estado impossível de prever. Testes verificam os casos que o autor imaginou; o fuzzer explora o espaço que ficou de fora. É especialmente indicado para parsers e qualquer código que receba dados de fora — exatamente onde um atacante tem controle sobre a entrada.

Exercício 4

O que o Miri detecta que nem os testes nem o compilador detectam?

Ver resposta

✓ Resposta: Comportamento indefinido dentro de unsafe: ponteiro desalinhado ou fora da alocação, leitura de memória não inicializada, violação das regras de aliasing, uso após liberação. O binário compilado pode executar isso "funcionando" por acaso durante anos e quebrar numa versão nova do compilador. O Miri interpreta o programa verificando essas regras a cada passo — é lento, o que o torna adequado à suíte de testes e não à produção, e só examina os caminhos que os testes efetivamente executam.

Exercício 5

Descreva um fluxo mínimo de auditoria de segurança para uma crate.

Ver resposta

✓ Resposta: cargo audit para confrontar as dependências com o banco de vulnerabilidades do RustSec; cargo deny para bloquear licenças indesejadas e dependências duplicadas; cargo clippy com os lints de correção ativados; cargo miri test se houver unsafe; e fuzzing contínuo do que interpreta entrada externa. Tudo isso na integração contínua, porque o valor está em rodar a cada mudança — auditoria feita uma vez envelhece no dia seguinte, quando sai a próxima vulnerabilidade.

Fontes e leituras recomendadas

Ferramentas

  • Rust Fuzz Book — Guia oficial de fuzzing em Rust, cobrindo cargo-fuzz e afl, da instalação à interpretação dos casos encontrados.
  • Miri — O interpretador que detecta comportamento indefinido, com a lista do que ele consegue e do que não consegue verificar.
  • RustSec Advisory Database — O banco de vulnerabilidades conhecidas das crates, que alimenta o cargo-audit.
  • cargo-deny — Verifica licenças, dependências duplicadas, fontes permitidas e avisos de segurança numa passada só.

Referência

  • The Rustonomicon — O manual do Rust inseguro: o que exatamente constitui comportamento indefinido e quais invariantes precisam ser mantidas.
  • Secure Rust Guidelines — ANSSI — Guia de desenvolvimento seguro publicado pela agência francesa de cibersegurança, com recomendações verificáveis.
Comentários

Mais em Rust

Unsafe Rust — Quando e Como Usar Código Inseguro com Responsabilidade
Unsafe Rust — Quando e Como Usar Código Inseguro com Responsabilidade

Unsafe não desliga o verificador de empréstimos nem os tipos: habilita cinco…

Autenticação JWT — Protegendo sua API com Tokens
Autenticação JWT — Protegendo sua API com Tokens

Token assinado não é token secreto: o payload de um JWT é legível por qualquer…

Smart Pointers — Box, Rc e RefCell
Smart Pointers — Box, Rc e RefCell

Quando as regras de posse não bastam, entram os ponteiros inteligentes. O…