Carga de archivos
Es muy común que las aplicaciones, en algún momento u otro, necesiten permitir a los usuarios subir un archivo (ya sea para su uso o simplemente para su almacenamiento) en algún lugar dentro de la aplicación. Si bien parece bastante simple, la forma en que se implementa esta función puede ser bastante crítica debido a los riesgos potenciales asociados con la forma en que se manejan las cargas de archivos.
Eche un vistazo a este ejemplo rápido para comprender mejor lo que queremos decir.
Digamos que se trata de una aplicación que permite a los usuarios subir una foto de perfil:
public string UploadProfilePicture(FormFile uploadedFile)
{
// Generate path to save the uploaded file at
var path = $"./uploads/avatars/{request.User.Id}/{uploadedFile.FileName}";
// Save the file
var localFile = File.OpenWrite(path);
localFile.Write(uploadedFile.ReadToEnd());
localFile.Flush();
localFile.Close();
// Update the profile picture
UserProfile.UpdateUserProfilePicture(request.User, path)
return path;
}
Esta sería una función de carga muy básica que también resulta ser vulnerable a Path Traversal.
Dependiendo de la implementación exacta de la aplicación, un atacante podría cargar otra página/script (piense en archivos .asp, .aspx o .php) que permitiría llamar directamente y ejecutar código arbitrario. Esto también podría permitir la anulación de archivos existentes.
Problema 1 - Guardar en el disco local en lugar de en un almacén de datos externo
A medida que se generaliza el uso de servicios en la nube, las aplicaciones se entregan en contenedores, las configuraciones de alta disponibilidad se han convertido en estándar y la práctica de escribir los archivos cargados en el disco local de la aplicación debería evitarse básicamente a toda costa.
Los archivos deben cargarse en una forma de almacenamiento central siempre que sea posible (almacenamiento en bloque o base de datos). Esto puede evitar clases enteras de vulnerabilidades de seguridad en este caso.
Problema 2 - No se validan las extensiones
En muchos casos en los que se explota una vulnerabilidad de carga de archivos, se basa en la capacidad de cargar un archivo con una extensión específica. Por lo tanto, es muy recomendable utilizar una lista de extensiones permitidas para los archivos que se pueden cargar.
Asegúrese de utilizar los métodos proporcionados por su lenguaje/framework para obtener la extensión del archivo para evitar problemas, como la inyección de bytes nulos.
También puede ser tentador validar el tipo de contenido de la carga, pero hacerlo puede hacerlo muy frágil, dado que los tipos de contenido utilizados para archivos específicos pueden diferir entre sistemas operativos. Además, en realidad no te dice nada sobre el archivo en sí, ya que el tipo de contenido es puramente un mapeo de una extensión.
Problema 3 - No se impide atravesar la ruta
Otro problema común con las subidas de archivos es que también tienden a ser vulnerables al path traversal. Esto es un tema aparte, así que en lugar de intentar resumirlo aquí, echa un vistazo a la guía completa sobre Path Traversal.
Más ejemplos
A continuación, te ofrecemos algunos ejemplos más de cargas de archivos seguras e inseguras.
C# - Inseguro
public string UploadProfilePicture(IFormFile uploadedFile)
{
// Generate path to save the uploaded file at
var path = $"./uploads/avatars/{request.User.Id}/{uploadedFile.FileName}";
// Save the file
var localFile = File.OpenWrite(path);
localFile.Write(uploadedFile.ReadToEnd());
localFile.Flush();
localFile.Close();
// Update the profile picture
UserProfile.UpdateUserProfilePicture(request.User, path)
return path;
}
C# - Seguro
public List<string> AllowedExtensions = new() { ".png", ".jpg", ".gif"};
public string UploadProfilePicture(IFormFile uploadedFile)
{
// NOTE: The best option is to avoid saving files to the local disk.
var basePath = Path.GetFullPath("./uploads/avatars/");
// Prevent path traversal by not utilizing the provided file name. Also needed to avoid filename conflicts.
var newFileName = GenerateFileName(uploadedFile.FileName);
// Generate path to save the uploaded file at
var canonicalPath = Path.Combine(basePath, newFileName);
// Ensure that we did not accidentally save to a folder outside of the base folder
if(!canonicalPath.StartsWith(basePath))
{
return BadRequest("Attempted to save file outside of upload folder");
}
// Ensure only allowed extensions are saved
if(!IsFileAllowedExtension(uploadedAllowedExtensions))
{
return BadRequest("Extension is not allowed");
}
// Save the file
var localFile = File.OpenWrite(canonicalPath);
localFile.Write(uploadedFile.ReadToEnd());
localFile.Flush();
localFile.Close();
// Update the profile picture
UserProfile.UpdateUserProfilePicture(request.User, canonicalPath)
return path;
public bool GenerateFileName(string originalFileName) {
return $"{Guid.NewGuid()}{Path.GetExtension(originalFileName)}";
}
public bool IsFileAllowedExtension(string fileName, List<string> extensions) {
return extensions.Contains(Path.GetExtension(fileName));
}