Directrices

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));
}

Java - Inseguro

@Controller
public class FileUploadController {

   @RequestMapping(value = "/files/upload", method = RequestMethod.POST)
   @ResponseBody
   public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file, @AuthenticationPrincipal User user) {

       try {

           String uploadPath = "./uploads/avatars/" + principal.getName() + "/" + file.getOriginalFilename();

           File transferFile = new File(uploadPath);
           file.transferTo(transferFile);

       } catch (Exception e) {
           return new ResponseEntity<>("Upload error", HttpStatus.INTERNAL_SERVER_ERROR);
       }

       return new ResponseEntity<>(uploadPath, HttpStatus.CREATED);
   }
}

Java - Seguro

@Controller
public class FileUploadController {

    @RequestMapping(value = "/files/upload", method = RequestMethod.POST)
    @ResponseBody
    public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file, @AuthenticationPrincipal User user) {

        try {
            String baseFolder = Paths.get("./uploads/avatars/").normalize();
            String uploadPath = Paths.get(baseFolder.toString() +
GenerateFileName(file.getOriginalFilename())).normalize();
           // Make sure that the extension is an allowed type
            if(!IsAllowedExtension(file.getOriginalFilename()) {
                return new ResponseEntity<>("Extension not allowed", HttpStatus.FORBIDDEN);
            }

            // Make sure that the file is not saved outside of the upload root
           if(!uploadPath.toString().startsWith(baseFolder.toString()))            {
                return new ResponseEntity<>("Files are not allowed to be saved outside of the base folder.", HttpStatus.FORBIDDEN);
           }

            File transferFile = new File(uploadPath.toString());
            file.transferTo(uploadPath.toString());

        } catch (Exception e) {
            return new ResponseEntity<>("Upload error", HttpStatus.INTERNAL_SERVER_ERROR);
        }

        return new ResponseEntity<>(uploadPath, HttpStatus.CREATED);
    }

    private string GenerateFileName(String fileName) {
        return UUID.randomUUID().toString() + "." + FilenameUtils.getExtension(fileName);
    }

    private boolean IsAllowedExtension(String fileName) {
        String[] allowedExtensions = {"jpg", "png", "gif"};
        String extension = FilenameUtils.getExtension(filename);
        return allowedExtensions.contains(extension);
    }
}

Python - Flask - Inseguro

@app.route('/files/upload', methods=['POST'])
def upload_file():

file = request.files['file']

savedFilePath = os.path.join("./uploads/avatars/", file.filename)
file.save(savedFilePath)

return savedFilePath

Python - Flask - Seguro

@app.route('/files/upload', methods=['POST'])
def upload_file():

file = request.files['file']
baseFolder = os.path.normpath("./uploads/avatars/")
savedFilePath = os.path.normpath(os.path.join(baseFolder, generate_file_name(file.filename)))

# Asegúrese de que la extensión está en el conjunto permitido
if not is_extension_allowed(file.filename):
return "This extension is not allowed"

# Asegúrese de que el archivo en el que estamos intentando guardar no está fuera de la base
if not savedFilePath.startsWith(baseFolder):
return "Se ha intentado guardar el archivo fuera de la carpeta base"

file.save(savedFilePath)

return savedFilePath

def generate_file_name(filename):
return str(uuid.uuid4()) + os.path.splitext(filename)[1]

def is_extension_allowed(filename):
return os.path.splitext(filename)[1] in (".png", ".jpg", ".gif")