Incluir medidas de seguridad al subir archivos a un servidor desde PHP es muy importante, ya que puede ser usado por atacantes para subir archivos maliciosos.

Subir archivos a un servidor es algo que está presente en casi todas las aplicaciones web y apps que se usan hoy en día.  Cuando se permite subir archivos a usuarios, se abre una puerta de hackeo para posibles atacantes. En muchos casos subir archivos es un requisito indispensable para el funcionamiento ciertas plataformas como por ejemplo redes sociales, blogs, foros, etc. En dichas plataformas los usuarios pueden subir imágenes, videos u otros tipos de documentos. Cuantos más tipos de archivos se permiten subir, mayor es el riesgo ante la posible subida de un archivo malicioso.

subir archivo

En este post voy a explicar conceptos y medidas de seguridad a implementar para programar una subida de archivos segura en PHP. Junto a cada medida de seguridad se añade un ejemplo de un posible ataque a sufrir y sus consecuencias en caso de no tomar la medida de seguridad correspondiente.

Formulario para subir archivos desde PHP

Para comenzar hay que tener claro cómo se construye un formulario para subir archivos desde PHP. La estructura básica que puede tener sería la siguiente:


<!-- Formulario -->

<form enctype="multipart/form-data" action="subirArchivo.php" method="POST">
    <input type="hidden" name="MAX_FILE_SIZE" value="100000" /> Elige el archivo a subir:
    <input name="uploadedfile" type="file" />
    <input type="submit" name="button" value="Subir archivo" />
</form>

<!-- Script PHP -->
<?php
    $directorio  =  "uploads/";
    $fichero  =  $directorio  .  basename($_FILES['uploadedfile']['name']);
    if(isset($_POST['button'])){
        if (move_uploaded_file($_FILES['uploadedfile']['tmp_name'], $fichero)) {
            echo "El archivo " . basename($_FILES['uploadedfile']['name']) . " se ha subido con éxito";
        } else {
            echo "Hubo un error subiendo el archivo, por favor inténtalo de nuevo!";
        }   
    }
?>

Al realizar el submit del formulario mediante POST request con el tipo de codificación multipart/form-data, se crea un archivo temporal con un nombre aleatorio. PHP crea un Array superglobal que se almacena en la variable global $_FILES (anteriormente se utilizaba HTTP_POST_VARS) con la información del archivo subido. La estructura del Array $_FILES es la siguiente:


Array
(
    [attachment] => Array
    (
        [name] => imagen.jpeg // Nombre original del archivo
        [type] => image/jpeg // MIME Type del archivo
        [tmp_name] => /tmp //Directorio temporal en el que se almacenará el archivo
        [error] => 0 //tipo de error
        [size] => 1234 //tamaño del archivo en bytes
    )
)

Para mover el archivo de su directorio temporal a un directorio personalizado se está utilizando la función move_uploaded_file().  Para el caso del ejemplo, el directorio seleccionado es /uploads. Tal y como está construido actualmente el formulario de subida de archivos, un atacante puede subir archivos PHP o de cualquier otro lenguaje interpretable por el servidor con código malicioso.

Implementar seguridad en la subida de archivos

Validación en subida de archivos desde el lado del cliente

Hay una cosa clara que se debe tener frente a las validaciones realizadas desde el lado del cliente, y es, que nunca se debe confiar en sus validaciones, ya que son “fácilmente” modificables. Dicho esto, tampoco hay que pensar en no usarlas, pero si tener en cuenta que son más bien un tipo de obstaculo más, frente a un posible ataque.

Restringir el tipo de archivo a subir en un input file HTML

Por defecto el input de tipo file abre una ventana emergente en la que permite seleccionar uno o varios archivos de cualquier tipo. El atributo accept del tag input permite filtrar qué tipos de archivos subir. Es decir, se puede restringir la subida de archivos a solo imagenes, videos, etc.

El atributo accept toma una lista de valores separados por coma que pueden ser:

  • Un content-type. Puedes ver la lista completa de Mime Types aquí.
  • Una extensión de archivo (.jpg, pdf, wav)
  • Una de las cadenas: “audio/*”, “video/*” o “image/*”

Mediante este atributo el navegador interpreta que tipos de archivo se espera recibir. A continuación un ejemplo HTML en el que solo se pretende subir imagenes:





<form action="../../update.php" method="post" enctype="multipart/form-data" target="_blank"> 
    Sube una imagen:
    <input type="file" name="imgFile" accept="image/png, .jpeg, .jpg, image/gif">
    <input type="submit" value="Subir imagen">
</form>




Otro tipo de validación desde el lado del cliente podría ser usar expresiones regulares en el nombre del archivo que se sube. De esta forma se puede determinar su extensión para comprobar si está incluida en la lista de extensiones permitidas.

Validación en subida de archivos desde el lado del servidor

Comprobaciones básicas

Primeramente sería recomendable agregar al script php una comprobación de que el archivo ha sido subido antes de moverlo del directorio temporal al definitivo, esto se puede comprobar con la función de php is_uploaded_file(). Si no se agrega esta verificación podría llegar a engañarse a la aplicación spoofeando (“reemplazando”) el name=”button” por otro distinto. Por lo tanto, al realizarse el submit y no encontrar $_POST[‘button’], el archivo subido no se movería del directorio temporal al definitivo y podría permitir (dependiendo de la configuración del httpd.conf) acceder al directorio temporal.

El servidor estará seguro si los siguientes parámetros son están de la siguiente manera:


<Directory />
    Options FollowSymLinks
    AllowOverride None
</Directory>

Es muy importante no tener la opción Indexes habilitada para evitar exponer directorios que puedan resultar sensibles por diversas razones.

Comprobar MIME Type

Cuando un archivo es subido al servidor PHP establece el elemento del array $_FILES[‘uploadfile’][‘type’] en función del MIME Type proporcionado por el navegador web que el cliente este usando. Es un error validar un archivo dependiendo solo de este valor. Un atacante puede enviar peticiones HTTP de forma automática adjuntando un archivo con su MIME Type falsificado.

Comprobar archivos por extensión

Un paso adicional de seguridad sería crear una black list con las extensiones de archivos que no se quieran permitir subir. De esta forma si un archivo contiene una extensión de la black-list denegar su subida. Un inconveniente de esta comprobación es que es casi imposible tener una lista completa de todas las posibles extensiones que un atacante puede usar. Si la aplicación funciona en un entorno de hosting, generalmente interpretan una gran cantidad de lenguajes (Ruby, Phython, Perl, etc) y la lista de extensiones a denegar se puede extender demasiado.

Posible vulnerabilidad

Un atacante podría subir un archivo .htaccess con la siguiente linea de código:


AddType application/x-httpd-php.jpg

Esta linea le indica al servidor Apache que ejecute las imágenes .jpg como si fueran scripts php. Por lo tanto, ahora el atacante podría subir un archivo .jpg con código php dentro y obtener una gran cantidad de información del servidor, por ejemplo ejecutando la función phpinfo().

Comprobar archivos con extensiones dobles

En este caso en lugar de comprobar la extensión del archivo, hay que buscar las extensiones buscando el carácter ‘.’ (punto) en el archivo, y extraer el string posterior al punto.

Posible vulnerabilidad

Apache puede manejar archivos de más de una extensión siendo el orden de las extensiones irrelevante. Por ejemplo si un archivo ejemplo.html.es indica que es de tipo text/html. Si existe más de una extensión e indican el mismo tipo de meta-información, se usará el de la derecha, excepto para lenguajes y codificación de contenidos. Por ejemplo, si .jpg indica image/jpeg y .html indica que es text/html, entonces el archivo ejemplo.jpg.html se asocia con el MIME Type text/html.

Por lo tanto, un archivo con nombre ejemplo.php.abcd sería interpretado como PHP pudiendo llegar a ejecutarse. Esto solo funciona si la segunda extensión (abdcd) no corresponde a ninguna especificada en la lista de mime-types conocidos por el servidor. Mediante esta técnica el atacante podría subir un shell.php.abdcd sin que este archivo fuera denegado por la black list creada en la medida de seguridad anterior al no estar incluida la extensión “abcd” en ella, y por lo tanto ser ejecutado por el servidor.

El número de extensiones dobles puede llegar a ser infinito, lo que hace imposible poder crear una black list con todas las extensiones a denegar. Por ello, lo mejor es realizar el proceso contrario y crear una white list (lista blanca) en la que se define una lista de extensiones permitidas.

A pesar de contar con esta validación, puede no servir de nada según como este la configuración de Apache. Existen dos formas de configurar Apache para ejecutar código PHP:

  • Con la directiva AddHandler
    
    AddHandler php5-script.php
    
    
  • Con la directiva AddType

Si se usa la directiva AddHandler, todos los archivos con la extensión .php, incluidas las dobles como .php.jpg, se ejecutarán como script PHP.  Un atacante puede subir el fichero shell.php.jpg, saltarse la validación y ejecutar el código.

Comprobar el header de las imágenes con getimagesize() o exif_imagetype()

Si solamente se permite la subida de imagenes se suele comprobar el header de la imagen mediante la función getimagesize(), la cual devuelve el valor del tamaño de la imagen. Si el header es incorrecto, la función devuelve false. Por lo tanto, si un atacante intenta subir un script PHP camuflado en un archivo .jpg la función devolverá false.

Otra posible función de PHP a usar es exif_imagetype(). Se le pasa el nombre del archivo por parámetro y devuelve una constante que puede ser:


1  IMAGETYPE_GIF
2  IMAGETYPE_JPEG
3  IMAGETYPE_PNG
4  IMAGETYPE_SWF
5  IMAGETYPE_PSD
6  IMAGETYPE_BMP
7  IMAGETYPE_TIFF_II (intel byte order)
8  IMAGETYPE_TIFF_MM (motorola byte order)
9  IMAGETYPE_JPC
10  IMAGETYPE_JP2
11  IMAGETYPE_JPX
12  IMAGETYPE_JB2
13  IMAGETYPE_SWC
14  IMAGETYPE_IFF
15  IMAGETYPE_WBMP

Se puede comparar el resultado devuelto por la función con la constante que se espera recibir. De esta forma es muy facil comprobar si el archivo subido es una imagen o no.


if (exif_imagetype($_FILES['archivo']['tmp_name']) != IMAGETYPE_GIF && exif_imagetype($_FILES['archivo']['tmp_name']) != IMAGETYPE_JPEG)
{ 
    echo "Archivo no permitido"; 
}

Posible vulnerabilidad

Esta medida puede ser fácilmente saltada editando el archivo mediante un editor de imágenes como por ejemplo Gimp, y editando el comentario de la imagen donde se puede insertar el código PHP.  La aplicación verificará que efectivamente el archivo es una imagen pero al tener extensión PHP, el servidor interpretará el código del comentario de la imagen y tomará el resto del archivo como código basura sin llegar a tenerlo en cuenta. De esta forma la imagen mantendrá el header.

Restringir la carpeta de subidas con .htaccess

Otra medida adicional sería proteger el directorio en el que se suben los archivos mediante .htaccess. La finalidad es restringir la ejecución de archivos desde dicha carpeta.

Conclusión

Finalmente, los aspectos más importantes a realizar para proteger el servidor ante la posible subida de archivos maliciosos se resumen en:

  • Definir un archivo .htaccess que sólo permita el acceso a archivos con las extensiones especificadas.
  • No poner el .htaccess en el mismo directorio en el que se realizan las subidas de los archivos.
  • Subir los archivos en un directorio fuera del server root al ser posible
  • Evitar la sobreescritura de archivos (para evitar un posible cambiazo del .htaccess)
  • Crear una white list mime-types aceptados.
  • Generar un nombre de archivo aleatorio y agregar la extensión previamente generada.
  • Implementar las medidas de seguridad en el código PHP vistas en este post.

Un ejemplo de fichero .htaccess en el que solo se permiten archivos gif, png, jpg, y jpeg , y que previene de ataques por doble extensión, tendría el siguiente código:


deny from all
    <Files ~ “^w+.(gif|jpe?g|png)$”>
    order deny,allow
    allow from all
    </Files>
	

Espero que este post te haya servido para poder dotar de una mayor seguridad tu plataforma, o simplemente haber obtenido una percepción más amplia, sobre las posibles vulnerabilidades que pueden ser explotadas si no se tiene bien controlada la subida de archivos en un servidor.

Si quieres seguir aumentando la seguridad en tu plataforma no olvides visitar este otro post sobre como realizar una conexión FTP segura a tu servidor mediante SFTP: Conexión SSH y SFTP. Generar claves RSA para una conexión segura.

 

Comparte el post