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

2. cURL y su sintaxis URL, pero a lo griego

Estimado visitante, me gusta compartir contigo este proceso de aprendizaje de CakePHP y que no le des demasiada importancia a mis meteduras de pata. Ya sabes que no doy lecciones de nada y que sólo expongo mis problemas técnicos -por llamarlo de alguna forma-, humildes reflexiones y pensamientos. Para tí, conocedor de todo esto, hoy me meto en un terreno bastante desconocido por mí: la lingüística. Si conoces este tema y lees este post, siéntete libre de ampliarlo y corregir lo que creas conveniente.

Vayamos al grano. En algún momento tuve problemas para utilizar la clase Phoogle en mi servidor compartido porque su php.ini tiene deshabilitada la directiva de configuración allow_url_fopen y no puedo utilizar la función file_get_contents(). Pues bien, navegando por Internet descubrí este recurso que explica exactamente cómo se resuelve este problema: para que Phoogle funcione en un PHP con la directiva allow_url_fopen en off hay que cambiar

 

$addressData = file_get_contents($apiURL.urlencode($address));

por estas instrucciones:


      $ch = curl_init();

      $timeout = 0; // set to zero for no timeout

      curl_setopt($ch, CURLOPT_URL, $apiURL.urlencode($address));

      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

      curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);

      $addressData = curl_exec($ch);

      curl_close($ch);

Por otra parte, aquí explican que hay muchos servidores que deshabilitan la opción allow_url_fopen para que los programadores no puedan abrir, incluir o utilizar archivos remotos utilizando un URL. Sin embargo, existe una alternativa: la librería cURL.

Otra persona hubiera dedicado su tiempo a ver cómo funciona esta librería, o cómo está programada; a mí me hipnotizó la definición que leí en su sitio oficial, “cURL es una herramienta de la línea de comandos que sirve para transferir archivos con sintaxis URL”. Esta vez -a diferencia de otras- vi un significado nuevo en la palabra sintaxis, supongo que por el contexto y el momento. Sintaxis… 

sintaxis1

*He escrito la palabra en Word y creo que está bien así.

Los griegos dijeron sintaxis. Syn quiere decir junto y táxis significa colocar. Bueno, también dicen que syn es con y táxis es orden. En cualquier caso, la sintaxis es la subdisciplina de la lingüística que estudia los principios y las reglas que rigen el orden de las palabras en las oraciones. Ciertamente, la sintaxis ordena las palabras de una frase como mueclas burlescas. rostro, del son generalmente contorsiones Las para que se pueda construir algo con sentido para nosotros, Las muecas del rostro son, generalmente, contorsiones burlescas. ¿Te atreves con más?

Pero volvamos a la informática -y a la Wikipedia-. Todo esto me ha servido para recordar que los URL tienen que seguir una sintaxis:

esquema://autoridad/ruta?consulta#fragmento

Donde esquema es el protocolo que se utiliza para recuperar la información del recurso solicitado -http, https, ftp, etc-; autoridad es el nombre o dirección IP del servidor; ruta es una carpeta; y consulta son los parámetros de una consulta dinámica. En otras palabras, debemos seguir estas reglas para referirnos correctamente a un recurso particular de Internet; por ejemplo: https://tutorialcakephp.wordpress.com/2008/12/03/5-phoogle-file_get_contents-y-allow_url_fopen/.

En resumen, con cURL podemos obtener un recurso externo en un servidor que tenga deshabilitada la directiva allow_url_fopen, pero es necesario entender y respetar la sintaxis URL. La palabra sintaxis es un término lingüístico que también se utiliza en informática. La página principal del proyecto, http://curl.haxx.se/, explica cómo trabajar con cURL.

 

1. Move_uploaded_file y safe_mode: SAFE MODE Restriction in effect.

Tenía una galería muy mona de fotos que funcionaba bien en local pero se ha ido al traste en producción.

Los hechos

El código que viene a continuación es un ejemplo que ilustra este problema. En realidad, para subir fotos al servidor, no utilizo la función copy(), sino que, por cuestiones de seguridad, uso la función move_uploaded_file(), como recomiendan aquí.

La acción A (empresas_controller.php) ejecuta este código:


mkdir('/var/www/vhosts/midominio.com/httpdocs/app/webroot/img/micarpeta');

chmod('/var/www/vhosts/midominio.com/httpdocs/app/webroot/img/micarpeta', 0777);

Y la acción B (categorias_controller.php), ejecuta este otro:


mkdir('/var/www/vhosts/midominio.com/httpdocs/app/webroot/img/micarpeta/misubcarpeta');

chmod('/var/www/vhosts/midominio.com/httpdocs/app/webroot/img/micarpeta/misubcarpeta', 0777);

copy( '/var/www/vhosts/midominio.com/httpdocs/app/webroot/img/barco.jpg','/var/www/vhosts/midominio.com/httpdocs/app/webroot/img/micarpeta/misubcarpeta');

El herror espantoso

SAFE MODE Restriction in effect.  The script whose uid is 10013 is not allowed to access /var/www/vhosts/midominio.com/httpdocs/app/webroot/img/micarpeta owned by uid 48 [APP/controllers/categorias_controller.php, line 54]

El motivo

Después de haber leído lo que el tiempo me ha dejado acerca del modo seguro de PHP, he entendido que el sistema tipo Unix de mi servidor compartido asigna el uid 10013 a empresas_controller.php y el 48 a la carpeta /var/www/vhosts/midominio.com/httpdocs/app/webroot/img/micarpeta.

Como el PHP se ejectua en modo seguro, en el momento en que el script categorias_controller.php ejecuta la instrucción


mkdir('/var/www/vhosts/midominio.com/httpdocs/app/webroot/img/micarpeta/misubcarpeta');

el intérprete de PHP lanza el error porque ve que los identificadores 48 y 10013 no coinciden.

Es decir, tal y como explica el manual de php, cuando el modo seguro está activado, PHP comprueba si los directorios y archivos que utiliza el script tienen el mismo UID que éste. Efectivamente, los uid no coinciden e inicialmente uno puede pensar, como yo, que la acción cuyo propietario es 10013 (uno mismo) intenta acceder a la carpeta que pertenece a 48 (uno mismo, también).

“¿Pero por qué, si soy el mismo?” Pues no. Finalmente resulta que la instrucción mkdir, que utiliza el script PHP, crea un directorio nuevo en el sistema de archivos tipo Unix de mi servidor compartido y el sistema le asigna mi identificador de usuario Apache: 48. Por otra parte, como todos mis scripts de la carpeta /var/www/vhosts/midominio.com/ están ahí gracias al servidor FTP de mi proveedor -y al cliente que utilizo para subirlos- el uid que tienen asignado es el de mi identificador de usuario FTP.

Conclusión

Esto es lo que más o menos creo haber entendido. Se me escapan muchas cosas porque no sé configurar un servidor Apache ni un servidor FTP. El caso es que no puedo implementar mi galería de fotos porque tiene una acción CakePHP que crea carpetas en un directorio y los usuarios no pueden tener su propia carpeta multimedia porque PHP se ejecuta en modo seguro. ¿Qué hago?