Directrices

Carga de archivos

Es muy habitual que las aplicaciones necesiten, en algún momento, permitir que los usuarios carguen un archivo (ya sea para su uso o simplemente para almacenamiento) en algún lugar de la aplicación. Aunque parece sencillo, la forma en que se implementa esta función puede ser crítica debido a los riesgos potenciales asociados con el manejo de dichas cargas.

Echa un vistazo a este breve ejemplo para comprender mejor a qué nos referimos.

Supongamos que esta es una aplicación que permite a los usuarios cargar una foto de perfil:

public string UploadProfilePicture(FormFile uploadedFile)
{
    // Generar la ruta donde guardar el archivo cargado
    var path = $"./uploads/avatars/{request.User.Id}/{uploadedFile.FileName}";  

    // Guardar el archivo
    var localFile = File.OpenWrite(path);
    localFile.Write(uploadedFile.ReadToEnd());
    localFile.Flush();
    localFile.Close();

    // Actualizar la foto de perfil
    UserProfile.UpdateUserProfilePicture(request.User, path)

    return path;
}

Esta sería una función de carga muy básica que, además, es vulnerable a ataques de recorrido de directorio (Path Traversal).

Dependiendo de la implementación exacta de la aplicación, un atacante podría cargar otra página o script (como archivos .asp, .aspx o .php) que permitiría llamar y ejecutar código arbitrario directamente. Esto también podría permitir sobrescribir archivos existentes.

Problema 1: Guardar en el disco local en lugar de en un almacén de datos externo

A medida que el uso de servicios en la nube se vuelve más común y las aplicaciones se entregan en contenedores, las configuraciones de alta disponibilidad se han convertido en el estándar. Por ello, la práctica de escribir archivos cargados en el disco local de la aplicación debería evitarse a toda costa.

Siempre que sea posible, los archivos deben cargarse en un almacenamiento central (almacenamiento en bloques o base de datos). Esto puede evitar categorías enteras de vulnerabilidades de seguridad en este caso.

Problema 2: No validar las extensiones

En muchos casos en los que se explota una vulnerabilidad de carga de archivos, el ataque depende de la capacidad de cargar un archivo con una extensión específica. Por lo tanto, es muy recomendable utilizar una "lista blanca" de extensiones permitidas para los archivos que se pueden cargar.

Asegúrate de utilizar los métodos proporcionados por tu lenguaje o framework para obtener la extensión del archivo y así evitar problemas como la inyección de bytes nulos (null-byte injection).

También puede resultar tentador validar el tipo de contenido (content type) de la carga, pero hacerlo puede hacer que el sistema sea muy frágil, dado que los tipos de contenido utilizados para archivos específicos pueden variar entre sistemas operativos. Además, esto no aporta información real sobre el archivo en sí, ya que el tipo de contenido es simplemente una asignación basada en la extensión.

Problema 3: No prevenir el recorrido de directorio (Path Traversal)

Otro problema común con la carga de archivos es que suelen ser vulnerables al recorrido de directorio. Es un tema extenso por sí mismo, 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, presentamos algunos ejemplos más de cargas de archivos seguras e inseguras que puedes consultar.

C# - Inseguro

public string UploadProfilePicture(IFormFile uploadedFile)
{
    // Generar la ruta donde guardar el archivo cargado
    var path = $"./uploads/avatars/{request.User.Id}/{uploadedFile.FileName}";

    // Guardar el archivo
    var localFile = File.OpenWrite(path);
    localFile.Write(uploadedFile.ReadToEnd());
    localFile.Flush();
    localFile.Close();

    // Actualizar la foto de perfil
    UserProfile.UpdateUserProfilePicture(request.User, path)

    return path;
}

C# - Seguro

public List<string> AllowedExtensions = new() { ".png", ".jpg", ".gif"};

public string UploadProfilePicture(IFormFile uploadedFile)
{
    // NOTA: La mejor opción es evitar guardar archivos en el disco local.
    var basePath = Path.GetFullPath("./uploads/avatars/");

    // Evitar el recorrido de rutas no utilizando el nombre de archivo proporcionado. También es necesario para evitar conflictos de nombres.
    var newFileName = GenerateFileName(uploadedFile.FileName);

    // Generar la ruta donde guardar el archivo subido
    var canonicalPath = Path.Combine(basePath, newFileName);

    // Asegurarse de no guardar accidentalmente en una carpeta fuera de la carpeta base
    if(!canonicalPath.StartsWith(basePath))
    {
        return BadRequest("Intento de guardar archivo fuera de la carpeta de subidas");
    }

    // Asegurarse de que solo se guarden extensiones permitidas
    if(!IsFileAllowedExtension(uploadedAllowedExtensions))
    {
        return BadRequest("Extensión no permitida");
    }

    // Guardar el archivo
    var localFile = File.OpenWrite(canonicalPath);
    localFile.Write(uploadedFile.ReadToEnd());
    localFile.Flush();
    localFile.Close();

    // Actualizar la foto de perfil
    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<>("Error de carga", 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();
           // Asegurarse de que la extensión sea de un tipo permitido
            if(!IsAllowedExtension(file.getOriginalFilename()) {
                return new ResponseEntity<>("Extensión no permitida", HttpStatus.FORBIDDEN);
            }

            // Asegurarse de que el archivo no se guarde fuera de la raíz de subidas
           if(!uploadPath.toString().startsWith(baseFolder.toString()))            {
                return new ResponseEntity<>("No se permite guardar archivos fuera de la carpeta base.", HttpStatus.FORBIDDEN);
           }

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

        } catch (Exception e) {
            return new ResponseEntity<>("Error de carga", 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)))

    # Asegurarse de que la extensión esté en el conjunto permitido
    if not is_extension_allowed(file.filename):
        return "Esta extensión no está permitida"

    # Asegurarse de que el archivo que intentamos guardar no esté fuera de la base
    if not savedFilePath.startsWith(baseFolder):
        return "Intento de guardar 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")
Eslogan

Programación segura y práctica

Explore las directrices de programación segura para comprender y mitigar vulnerabilidades de software como el OWASP Top 10, y sumérjase en misiones de formación guiada para practicar en simulaciones de aplicaciones del mundo real.

Más directrices
Reservar una demostración