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 = "teste injetado";
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-audite RustSec Advisory Database — banco de vulnerabilidades — https://rustsec.orgcargo-fuzzdocumentation — https://rust-fuzz.github.io/book/proptestbook — https://proptest-rs.github.io/proptest/- Miri documentation — https://github.com/rust-lang/miri
zeroizecrate — zeragem segura de memória — https://docs.rs/zeroizesubtlecrate — 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