La vulnerabilidad de siempre, con ropa nueva
Pasar una cadena del usuario a un shell. Por qué es la misma familia que la inyección de consultas, cómo se ve en una app de escritorio, y las tres reglas que la cierran.
Cómo aparece
// mal: se construye una orden como cadena y se le da a un shelllet orden = format!("convertir '{}' salida.mp4", nombre_del_fichero);Command::new("sh").args(["-c", &orden]).output()?;Con un nombre normal funciona. Con un nombre que el usuario no eligió —un fichero descargado, importado, o que venía dentro de un archivo comprimido— la comilla se cierra y lo que sigue es otra orden.
La última fila es la que hace que esto se descubra pronto en el mejor caso: la misma debilidad que permite inyectar una orden hace que un fichero llamado «mi vídeo.mp4» falle. Si tu código funciona con espacios, probablemente sea porque los estás escapando — y escapar no basta.
Por qué escapar no es la solución
Es la respuesta intuitiva y es la equivocada, por la misma razón que en el nivel 41.4:
- Las reglas de escape dependen del shell, y no hay uno solo. Lo que basta en uno no basta en otro.
- Son sutiles: comillas, barras, saltos de línea, caracteres de expansión, sustitución de órdenes. Cada uno tiene su caso raro.
- Y una función de escape correcta hoy puede no cubrir un caso que aparezca mañana.
Escapar es intentar ganar un juego que no hace falta jugar. La alternativa correcta elimina el intérprete en vez de intentar domarlo.
Las tres reglas
La regla 3 es la que se olvida cuando alguien ya aplicó las dos primeras. Un fichero llamado --output=/etc/algo no inyecta nada en ningún shell y sí puede confundir al programa que lanzas. La defensa es normalizar la ruta y comprobar que apunta donde debe — lo del nivel 28.
Cuándo hace falta un shell de verdad
Casi nunca, y conviene saber los dos casos:
- Cuando el usuario escribe la orden. Una aplicación que ofrece una consola o un campo de órdenes personalizadas: ahí el usuario está pidiendo ejecutar lo que escriba, y eso es la función. Lo que hay que garantizar es que solo puede venir del usuario, nunca de un dato.
- Cuando necesitas expansión o tuberías. Y casi siempre se puede hacer en Rust: lanzar dos procesos conectados no necesita shell.
Si te ves usando un shell por comodidad, es señal de que hay una forma directa que no has buscado.
Detectarlo en código existente
Tres búsquedas de un minuto:
- Cualquier uso de un shell con el argumento que ejecuta una cadena.
- Cualquier construcción de cadena que acabe siendo un argumento de un lanzamiento.
- Y cualquier
argscon un solo elemento que contenga espacios: casi seguro que alguien pensó que se partiría solo.