Seguridad de aplicaciones Rust: top 10 vulnerabilidades en la práctica
Rust se posiciona como un lenguaje de sistemas seguro, capaz de prevenir clases enteras de errores típicos de C y C++. Sin embargo, esto no significa que las aplicaciones Rust sean invulnerables. El sistema de ownership y el borrow checker protegen contra muchos problemas, pero el código unsafe, los errores lógicos y las dependencias externas abren nuevos vectores de ataque. En este artículo, analizaremos las 10 vulnerabilidades más críticas que enfrentan los desarrolladores de Rust, con ejemplos de código reales y consejos prácticos para solucionarlas.
El material será útil tanto para principiantes como para ingenieros experimentados que deseen profundizar sus conocimientos sobre seguridad en el ecosistema Rust.
1. Uso inseguro de unsafe
La palabra clave unsafe es la fuente más común de vulnerabilidades en Rust. Desactiva las comprobaciones del borrow checker y permite trabajar con punteros sin procesar, pero la responsabilidad de la corrección recae completamente en el desarrollador.
// Código vulnerable: desreferencia de puntero nulofn dangerous() { let ptr: *const i32 = std::ptr::null(); unsafe { println!("{}", *ptr); // UB! Segfault }}Solución: minimice el uso de unsafe. Aísle las operaciones peligrosas en módulos separados con pruebas exhaustivas y documentación de invariantes. Use NonNull<T> en lugar de punteros sin procesar.
2. Condiciones de carrera (Data Races)
A pesar de que el borrow checker previene las carreras en código seguro, el uso de unsafe y el mal uso de primitivas de sincronización pueden provocar condiciones de carrera.
// Código vulnerable: condición de carrera a través de unsafestatic mut COUNTER: i32 = 0;
fn increment() { unsafe { COUNTER += 1; // Operación no atómica }}Solución: use AtomicI32, Mutex o RwLock. En código Rust seguro, las condiciones de carrera son imposibles: confíe en el compilador.
3. Integer Overflow (desbordamiento de enteros)
En modo debug, Rust entra en pánico ante un desbordamiento, pero en modo release, por defecto, se produce un wrapping (acarreo cíclico), lo que puede provocar un comportamiento inesperado.
// Vulnerabilidad potencial: lógica de desbordamientofn allocate_buffer(size: usize) { let header_size = 64; let total = size + header_size; // Puede desbordarse let buf = vec![0u8; total]; // Pánico o buffer pequeño}Solución: use los métodos checked_add(), saturating_add() o una verificación explícita. Para cálculos críticos, use el crate overflow-checks.
4. Fugas de memoria en código unsafe
Rust garantiza la ausencia de fugas en código seguro, pero unsafe puede violar este principio. Por ejemplo, una gestión incorrecta de punteros sin procesar o referencias cíclicas con Rc.
// Fuga de memoria: olvidó llamar a dropfn leak_memory() { let ptr = Box::into_raw(Box::new(42)); // ptr no se libera — fuga}Solución: use Box::from_raw() para una liberación correcta. Para referencias cíclicas, use Weak o Arc.
5. Llamadas FFI inseguras
Las llamadas a funciones C a través de FFI (Foreign Function Interface) son una de las principales fuentes de vulnerabilidades. Rust no puede controlar la seguridad del código externo.
// FFI peligroso: paso de puntero incorrectoextern "C" { fn c_free(ptr: *mut c_void);}
fn free_wrapper(ptr: *mut c_void) { unsafe { c_free(ptr); } // ¿Doble liberación?}Solución: use wrappers seguros, verifique los límites de los búferes, aplique std::ffi::CStr para str