3. Subir imágenes al servidor y seguridad: Image Upload Component

Estoy dedicando esta mañana a mejorar (o eso es lo que yo creo) algunas partes de mi primera aplicación CakePHP y ahora estoy en la que se encarga de subir imágenes al servidor. En el controlador EmpresasController he escrito una lógica para esto, y, aunque he seguido varios tutoriales y blogs, estoy un poco intranquilo por el tema de la seguridad.

Cuando implementé la subida de imágenes dudaba entre guardarlas en una carpeta del servidor y guardarlas directamente en la base de datos. Aunque seguí en CakeBaker esta explicación de Daniel Hofstetter, uno de los colaboradores del proyecto CakePHP, al final me inspiré en la idea que propone James Fairhurst aquí. También me descargué este documento, escrito por Alla Bezroutchko, de Scanit, que explica muy bien cómo implementar una subida de archivos segura en PHP.  De la idea de James Fairhurst y el documento de Alla Bezroutchko salió este código:

class SubirfotoComponent extends Object {
/**
   Método que comprueba la extensión de una imagen.</pre>
 

   Este método sirve para que un atacante no pueda crear una imagen,
   con un editor de imágenes, y le añada un comentario con código PHP.

 

   Un archivo puede ser a la vez una imagen GIF o JPG y un script PHP porque hay editores
   de imágenes que permiten añadir comentarios (GIMP, por ejemplo) y un atacante puede
   añadir un comentario que sea código PHP interpretable. Tan sólo hay que crear la imagen
   GIF (foto.gif), añadir código PHP en el comentario, y cambiarle la extensión (foto.php).      .
   */

 

   private function compruebaExtension($nombreArchivo){

      $esBueno = true;

      $listanegra = array(".php",".phtml",".php3",".php4");

      foreach($listanegra as $item){

         if (preg_match("/$item\$/i", $nombreArchivo)){

            $esBueno = false;

         } // fin de if (preg_match("/$item\$/i", $nombreArchivo))

      } // fin de foreach($listanegra as $item)

      return $esBueno;

   } //fin de function compruebaExtension($nombreArchivo)

   /**
      Método que comprueba la validez de una foto.

 

      No confiamos en los datos que envía el usuario y no vamos a comprobar el tipo MIME del archivo
      porque un atacante podría crear un script con extensión .gif, por ejemplo, y manipular el tipo.

 

      Hacemos la validación en el lado del servidor con la función getimagesize().
      La función getimagesize() toma una cadena como nombre de la imagen y devuelve el tamaño
      (en píxels, no en bytes) y el tipo de la imagen, después de haber comprobado que, efectivamente,
      se trata del contenido de una imagen.

 

      Este método necesita el nombre temporal del archivo -el que el usuario selecciona de su sistema
      de archivos-, el tamaño de la foto en bytes y el nombre de la foto y su extensión. Todos estos
      datos se obtienen del $data del formulario, que contiene el input de tipo archivo que necesitamos.

 

      @param cadena $nombreTemporal Nombre temporal del archivo que selecciona el usuario en su sistema.
      @param entero $tamanyoFoto Tamaño de la foto en bytes.
      @param cadena $nombreFoto Nombre y extensión de la foto.
      @return boolean Devuelve true si la foto es válida y false en caso contrario.
      @access private
   */

 

   function validar($nombreTemporal,$tamanyoFoto,$nombreFoto){

      $esBueno = false;

      /* Si los parámetros están vacíos, entonces el archivo es bueno. */

      if (empty($nombreTemporal) && empty($tamanyoFoto) && empty($nombreFoto)) $esBueno = true;

      $imageInfo = getimagesize($nombreTemporal);

         if ($imageInfo['mime'] == 'image/gif' || $imageInfo['mime'] == 'image/jpeg'){

            if ($tamanyoFoto <= 524288){

               if ($this->compruebaExtension($nombreFoto)) {

                  $esBueno = true;

               } // fin de if ($this->compruebaExtension($nombreFoto))

            } // fin de if ($tamanyoFoto <= 524288)

         } // fin de if if ($imageInfo&#91;'mime'&#93; == 'image/gif' || $imageInfo&#91;'mime'&#93; == 'image/jpeg')

      return $esBueno;

   } // fin de function validarFoto($nombreTemporal,$tamanyoFoto,$nombreFoto)

   /**
      Método para mover el archivo que ha subido el usuario de la carpeta temporal a la ubicación
      especificada en $destino.
   */

 

   function copiar($origen, $destino){

      move_uploaded_file($origen, $destino);

   }

}

&#91;/sourcecode&#93;

Aún así, como explico un poco más adelante, al final -aunque todavía no lo he probado- creo que me quedaré con el <a href="http://labs.iamkoa.net/2007/10/23/image-upload-component-cakephp/" target="_blank">Image Upload Component</a> de <a href="http://www.labs.iamkoa.net">www.labs.iamkoa.net</a> para subir las imágenes al servidor. El objetivo de este post es dejar en algún sitio lo que utilicé en algún momento para subir las fotos; además, también es posible que en algún otro momento lo necesite.

Bueno, volvamos a mi código preocupante. He codificado el método estrella, <strong>validar()</strong>, en el componente <strong>SubirfotoComponent</strong>. Por otra parte, <strong>EmpresasController</strong> utiliza este <strong>validar()</strong> en algún lugar:



         /* Comprobamos si las fotografías del $data de paso5 son correctas*/
         $validaFoto1 = $this->Subirfoto->validar(   $this->data['Empresa']['foto1']['tmp_name'],
                                                     $this->data['Empresa']['foto1']['size'],
                                                     $this->data['Empresa']['foto1']['name']);
         $validaFoto2 = $this->Subirfoto->validar(   $this->data['Empresa']['foto2']['tmp_name'],
                                                     $this->data['Empresa']['foto2']['size'],
                                                     $this->data['Empresa']['foto2']['name']);

Finalmente, en la vista hay este código para que el usuario pueda subir sus dos fotografías al servidor:


   echo '
<div id="formulario">';
   echo $form->create('Empresa', array('action' => 'paso5', 'type' => 'file'));
   echo $form->input('foto1', array('type' => 'file', 'label' => 'Primera fotografía: ', 'between' => '
'));
   if (isset($mensajeFoto1)) echo $mensajeFoto1;
   echo $form->input('foto2', array('type' => 'file', 'label' => 'Segunda fotografía: ', 'between' => '
'));
   if (isset($mensajeFoto2)) echo $mensajeFoto2;
   echo '
';
   echo $form->end('Guardar');
   echo '</div>
<!--Fin de la división "formulario"-->';

Como explica el documento de scanit, en el momento de subir los archivos desde la vista, un atacante podría elegir, por ejemplo, un script .php. Pues bien, la instrucción


$imageInfo = getimagesize($nombreTemporal);

es una primera medida de seguridad.

Como dice la documentación oficial de PHP,  “La función getimagesize() determinará el tamaño de cualquier archivo de imagen dado y devuelve las dimensiones junto con el tipo de archivo y una cadena de texto de altura/ancho a ser usada en una etiqueta HTML IMG corriente y el tipo de contenido HTTP correspondiente”. Además de esto, getimagesize() comprueba que efectivamente el contenido del archivo que envía el usuario es el correspondiente a una imagen. De esta forma, el atacante no puede enviar un script de texto con extensión .php y manipular el Content-Type de la cabecera de petición POST para que parezca una imagen.

Sin embargo, esta comprobación no es suficiente porque hay programas que permiten crear imágenes y añadirles un comentario que se puede aprovechar para escribir código PHP (por ejemplo, Gimp). En otras palabras, como un archivo puede ser a la vez una imagen y un script PHP, un atacante puede enviar al servidor un script PHP con forma de imagen (con extensión .php) y getimagesize() lo dejaría pasar.

Por todo esto decía antes que el método estrella de SubirfotoComponent es validar(). En efecto, lo primero que hace validar() es utilizar getimagesize() para comprobar, como hemos dicho antes, que el usuario sube efectivamente una imagen. A continuación, comprueba si la extensión de la imagen es .gif o .jpg, verifica si el tamaño de la imagen es más pequeño que 524288 bytes y, finalmente, comprueba la extensión del archivo para que un atacante no envíe un script PHP con forma de imagen. Para hacer esto último, validar() utiliza el método compruebaExtension(), definido también en SubirfotoComponent, que consiste en una lista negra de extensiones que nunca dejaremos pasar.

Pero el documento de scanit sigue, explica más casos que se deben tener en cuenta en el momento de subir imágenes al servidor y yo no los contemplo todos. Además, por tratarse de mi primera aplicación CakePHP, es normal que no haya estructurado bien todo este código.

Por estas razones decía al principio que ando un poco preocupado con el tema de la seguridad y al final, pues, decido utilizar el Image Upload Component de Ben Borowski, disponible en www.labs.iamkoa.net. ¿Y tú, lo has probado? Si es así deja aquí algún comentario. ¿Qué tal es? Mientras tanto, yo voy a cambiar todo este código por el Image Upload Component, a ver qué tal.

Anuncios