Rust

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

20 min de leitura

Segurança e Auditoria — Fuzzing, Análise Estática e Hardening de Código Rust
Rust — Artigo #47 Segurança e Auditoria — Fuzzing, Análise Estática e Hardening de Código Rust Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano Rus

Rust — Artigo #47

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

Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano


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 == '' ||                          // null byte
        (c.is_control() && c != '
' && c != '	' && c != '
')
    }) {
        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 = "testeinjetado";
        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

Fontes e leituras recomendadas

  • "Rust Security Guidelines" — guia oficial da comunidade — https://anssi-fr.github.io/rust-guide/
  • cargo-audit e RustSec Advisory Database — banco de vulnerabilidades — https://rustsec.org
  • cargo-fuzz documentation — https://rust-fuzz.github.io/book/
  • proptest book — https://proptest-rs.github.io/proptest/
  • Miri documentation — https://github.com/rust-lang/miri
  • zeroize crate — zeragem segura de memória — https://docs.rs/zeroize
  • subtle crate — operações constante-time — https://docs.rs/subtle
  • "The Rustonomicon" — guia de unsafe Rust — https://doc.rust-lang.org/nomicon/
  • "Security Engineering" — Ross Anderson — fundamentos de segurança de sistemas

Artigo #47 de 52 | Série: Dominando Rust em 1 Ano Próximo → Artigo #48: Rust em Produção — Observabilidade, métricas e operação de sistemas Rust


Comentários

Mais em Rust

Banco de Dados com SQLx — Persistência Real para sua API
Banco de Dados com SQLx — Persistência Real para sua API

No artigo anterior construímos uma API REST completa — mas com um problema cr...

Variáveis, Tipos e a Arte da Imutabilidade
Variáveis, Tipos e a Arte da Imutabilidade

Em um artigo anterior, instalamos o Rust e entendemos por que ele existe. Hoj...

Parsing e Processamento de Texto — nom, pest e Expressões Regulares
Parsing e Processamento de Texto — nom, pest e Expressões Regulares

Parsing é uma das tarefas mais universais da programação. Toda vez que você l...