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
cargo auditno CI — bloqueie merges com vulnerabilidadescargo deny— controle licenças e fontes de dependências- Variáveis de ambiente — nunca hardcode segredos
secrecy— para valores sensíveis em memória- Queries parametrizadas — nunca concatene SQL
- Validação com tipos — newtype pattern para dados validados
- Minimize
unsafe— documente cada uso com# Safety - Headers de segurança — CSP, HSTS, X-Frame-Options
- Rate limiting — proteja APIs contra abuso
- Atualize dependências —
cargo outdatedregularmente
Veja Também
- Receita: Conectar ao PostgreSQL — Exemplos de queries parametrizadas com SQLx
- Boas Práticas de Error Handling — Trate erros sem expor informações sensíveis
- CI/CD para Projetos Rust — Automatize auditorias de segurança
- Receita: Variáveis de Ambiente — Como gerenciar configurações com segurança
- Logging e Observabilidade — Logging seguro sem vazar dados sensíveis
- Gerenciamento de Dependências — Controle suas dependências com Cargo