control de subida de archivos maliciosos, meme malware por todas partes
Auditoría de código

Control en subida de archivos maliciosos (con ejemplos de código)

Que tu aplicación implemente un control de seguridad aceptable en la subida de los archivos que recibe, tiene varios detalles que hay que revisar, aquí lo dividiremos en tres puntos de control con ejemplos en Java. No es un control que suela implementarse bien y de forma completa, sin embargo encontrar vulnerabilidades en él no es sencillo para una herramienta automática. Es por esta razón que muchas veces no nos aparece en los reportes, de por ejemplo herramientas como SonarQube.

Sin embargo es importante aplicarlo correctamente, por que los usuarios pueden infectarse con virus subidos por otro usuario, el sistema puede usarse para su distribución, e incluso puede llegar a infectarse el server donde se hospedan estos. Vamos a ver como podemos aplicar el control de seguridad para la subida de archivos.

Comprobar el tamaño y el nombre (texto del nombre de archivo y extensión)

En primer lugar, controlaremos nombre de archivo y tamaño, seguramente no tiene sentido que tu aplicación acepte adjuntar archivos «.EXE» de 300 megabytes, quizás ni siquiera tiene sentido que se suban «.EXE» en ese formulario. Tampoco debes aceptar nombres de archivo con juegos de caracteres permisivos, de un sistema de archivos Ext4 si después el server usa NTFS, etc…

Por un lado la propia aplicación debe implementar unas comprobaciones mínimas del archivo, el tamaño del binario que están subiendo debe tener un limite máximo, para rechazarlo en caso de super este. Este control debe implementarse tanto en la parte cliente o front end (para evitar consumo de tiempo y recursos innecesario, tanto para la aplicación como para el usuario), como en la parte server o back end (la protección necesaria y final del sistema). Si no implementas un control de tamaño puedes sufrir un ataque por denegación de servicio bastante sencillo.

Ejemplo de código seguro: en entorno tecnológico Java, control sobre el limite de tamaño de un archivo. Según el framework usado, se puede establecer un limite de la petición POST y de sus archivos a nivel de arquitectura, esto es lo correcto para ni siquiera tratar la petición en caso de tener un tamaño inadecuado. Después podemos centralizar el código que controle la entrada de archivos, aquí se muestra un control sobre las propias líneas de código de un controller REST, con la motivación de reducirlo a su ejemplificación básica.

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  Part filePart = request.getPart("file"); // Retrieves <input type="file" name="file">, received within a multipart/form-data POST request

  // getSize(): a long specifying the size of this part, in bytes
  if (filePart == null || filePart.getSize() == 0) {

  } else if (filePart.getSize() > 41943040) { // limit in 40MB

  }

  String fileName = getFilename(filePart);

  InputStream filecontent = filePart.getInputStream();
  // ... (continuar su tratamiento)
}

Por ilustrarlo con un ejemplo, desde el punto de vista de la seguridad, la subida deberá ser lo más restrictiva posible dentro de no afectar funcionalmente al sistema, no afectando al desempeño de sus usuarios y procesos. Poniendo un ejemplo de archivos adjuntos a un formulario, pongamos que solo se requiere subir archivos PDFs, podemos establecer los siguientes primeros controles:

  • El archivo debe venir con un nombre acabado en extensión PDF.
  • El archivo debe tener un tamaño menor de 40 megabytes.
  • El archivo debe venir con un nombre que conste únicamente de caracteres de una lista blanca (evitando caracteres reservados de sistemas de archivos como NTFS y ext4, y ataques asociados) y de una longitud establecida, por ejemplo 150 caracteres.
  • Debemos eliminar toda aparición de una ruta absoluta o parcial, el nombre del archivo debe ser solo eso, un nombre de archivo. Los ataques de tipo «Path Traversal», intentan cambiar de directorio mediante cadenas maliciosas como por ejemplo «/../», que intenta ir a un nivel superior, debemos usar una función que proteja de este tipo de ataques como por ejemplo la función java «FilenameUtils.getName(nombreArchivo)». Esta función antes indicada nos devuelve únicamente el nombre de archivo, y elimina otro tipo de concatenaciones de cadenas de texto maliciosas.

Cualquier incumplimiento de lo anterior rechaza la subida informando al usuario adecuadamente. 

Se recomienda establecer este control tanto en front end como en back end (parte cliente – parte servidor). La parte cliente para no consumir tiempo ni recursos de más, tanto para el usuario como para el sistema (se le rechaza cuanto antes), en la parte servidor para proteger verdaderamente en la parte final, la subida al servicio web

A continuación se muestra un ejemplo de comprobación en parte servidor, como es evidente el siguiente ejemplo es pseudocódigo, no se debe tomar como una referencia exacta. Cuanto mejor lo integres en tu entorno tecnológico y/o framework que emplees mejor, preferiblemente no reinventando la rueda y usando componentes ya desarrollados y contrastados. Ejemplo Java:

import org.apache.commons.io.FilenameUtils;
import java.util.regex.*;
 
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  Part filePart = request.getPart("file"); // Retrieves <input type="file" name="file">, received within a multipart/form-data POST request
 
  // getSize(): a long specifying the size of this part, in bytes
  if (filePart == null || filePart.getSize() == 0) {
    // Rechazar por estar el archivo vacío
  } else if (filePart.getSize() > 41943040) { // limit in 40MB
    // Rechazar por superar el archivo el tamaño permitido
  }
 
  String fileName = FilenameUtils.getName(filePart);
  String ext = FilenameUtils.getExtension(fileName);
  
  List extensionList = new ArrayList();
  extensionList.add("pdf");
  extensionList.add("doc");
  extensionList.add("docx");
  
  
  if (!list.contains(ext)) {
    //Rechazar por tener el archivo una extensión no permitida para esta subida
  }
  
  if (Pattern.compile("^[\w,\s-]+\.[A-Za-z]{3}$").matcher(fileName).matches()) {
    // Rechazar por caracteres no validos en el nombre de archivo o sencillamente sanitizarlo 
  }
  
  InputStream filecontent = filePart.getInputStream();
  // ... (continuar su tratamiento)
}

Comprobar el tipo de binario, obteniendo los primeros bytes del archivo

En este segundo punto, podemos comprobar si efectivamente el archivo es lo que dice ser analizando su contenido binario. Los archivos disponen de un comienzo llamado «Magic Bytes», de una longitud variable (los primeros 2-20 bytes), que indica el formato del resto del archivo. Esta identificación de caracteres alfanuméricos es requerida por los programas para trabajar con su contenido, y desde el punto de vista de la seguridad es positivo controlar que los binarios realmente contengan el formato que deberían.

Un atacante puede perfectamente falsear esta cabecera de bytes, disponiendo a continuación el contenido malicioso que desee, pero con ello también dificultará la ejecución involuntaria por parte de una víctima, por que al igual que tú código, otra aplicación usualmente también hará uso de la cabecera de bytes. 

chiste, imagen de cine antiguo de fiesta de disfraces con caretas espeluznantes, hace referencia a amenazas CTI
Archivos «normales» esperando su turno a subir al sistema

Continuando con el ejemplo anterior si la aplicación en esa funcionalidad solo debe recibir PDFs, los binarios deberán comenzar por la siguiente cabecera, ejemplo «%PDF-1.6»:

Representación ASCII: "%PDF-"
Hexadecimal: "25 50 44 46 2d"

Ejemplo de comando para ver la cabecera de bytes del archivo en bash:

head -c 10 *

Esta segunda comprobación, más profunda, puede estar centralizada en un servicio específico. Ejemplo siguiendo con Java, instanciando Apache Tika como dependencia en proyecto maven:

<dependency>
    <groupId>org.apache.tika</groupId>
    <artifactId>tika-core</artifactId>
    <version>1.12</version>
</dependency>
import java.io.File;
import org.apache.tika.Tika;

public class SimpleTypeDetector {

  //Ejemplos de cadenas de texto que especifican el tipo de archivo detectado
  public static final String MIME_TYPE_IMAGE_JPEG = "image/jpeg";
  public static final String MIME_TYPE_IMAGE_PNG = "image/png";
  public static final String MIME_TYPE_APPLICATION_PDF = "application/pdf";
  
  public static void main(String[] args) throws Exception {
    Tika tika = new Tika();

    for (String file: args) {
      //Llamada al método principal "detect()"
      String mimeType = tika.detect(new File(file));
      System.out.println(FilenameUtils.getName(file) + ": " + mimeType);
    }
  }
}

Comprobar mediante un sistema antivirus, si el binario tiene detecciones como malware

En este último punto enviamos el binario a un web service antivirus, que nos confirmara que este archivo no tiene registros maliciosos antes de seguir adelante. En caso de que si se detecte el binario como positivo de malware, es muy recomendable por parte de la gestión de seguridad, registrar este evento y avisar del incidente de seguridad

Esta tercera comprobación, más profunda, requiere de un servicio específico profesional antivirus como por ejemplo Kaspersky o ClamAV. El servicio de VirusTotal (si subirles los binarios no tiene problema con tu política de privacidad), tiene en cuenta múltiples servicios antivirus y es un referente mundial de búsqueda de indicadores de compromiso (referencia implementación de consumo del API del servicio de virustotal: https://support.virustotal.com/hc/en-us/articles/360006819798-API-Scripts-and-client-libraries).

  private static boolean isValidFile(FacesContext context, FileUpload fileUpload, List<Part> parts) throws IOException {
    long totalPartSize = 0;
    for (int i = 0; i < parts.size(); i++) {
      Part p = parts.get(i);
      totalPartSize += p.getSize();
      NativeUploadedFile uploadedFile = new NativeUploadedFile(p, fileUpload);
      if (!FileUploadUtils.isValidType(fileUpload, uploadedFile.getFileName(), uploadedFile.getInputstream())) {
        return false;
      }
      try {
        FileUploadUtils.performVirusScan(context, fileUpload, uploadedFile.getInputstream());
      }
      catch (VirusException ex) {
        return false;
      }
    }
    return fileUpload.getSizeLimit() == null || totalPartSize <= fileUpload.getSizeLimit();
  }
}

Pruebas del control y referencia

Para hacer pruebas dinámicas del control de seguridad puedes usar los recursos de EICAR, con ello ver si efectivamente el sistema nos impide la subida y nos alerta el antivirus, intentar usar diferentes formatos, tamaño, etc…

Referencia general, del proyecto OWASP:
https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html

Apache Tika in action: https://livebook.manning.com/book/tika-in-action/chapter-4/

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

error: