¿Cómo resolver un fallo al validar tipos mime para carga de imágenes en Zendframework 3x? – php zend

Pregunta:


Estoy desarrollando un módulo de extensión para Zendframework 3x (cuyos detalles no son importantes para el tema.) Este debe contar con un utilitario para carga de imágenes desde el cliente, de modo que las validaciones son muy importantes, particularmente por los ataques por suplantación de tipos y por incrustación de código malicioso en ciertos tipos de imágenes como .gif que hacen posible incluir bloques ejecutables dentro de comentarios).

Para resolver el asunto extendí la clase IsImage de ZF3 que ofrece un conjunto de validadores que parecen adecuados y prometen ahorrar mucho trabajo. Grosso modo:

class ImageMimeGeter extends IsImage {
    protected $tipoencontrado = '';

    public function isValid($value, $file = null) {
        $done = parent::isValid($value, $file);
        $this->tipoencontrado = ($done) ? $this->type : '';
        return $done;
    }

    public function tipoMime(){
        return $this->tipoencontrado;
    }
    // sigue código no pertinente para la consulta
}

Al construir instancias de esta clase para validar los tipos de los archivos de imagen que subo al servidor de desarrollo el proceso aborta y devuelve la típica página en blanco de ZF3 que revela un fallo grave. (Literalmente el “sistema se cae”).

Estuve haciendo validaciones revisando paso a paso los métodos que ejecutan la clase madre que extendí y las que aquella extiende y logré identificar el momento (llamado) que aborta el proceso de validación:

/* recojo la información de archivo en $_FILES y la formateo 
   correctamente para poderla pasar a los validadores 
   el método recogerImagenACargar() no existe lo supongoo para
   ilustrar la situación. (el mecanismo de formateo es complejo porque
   $_FILES es un array con varios formatos posibles que dependen de que 
   la carga sea individual o múltiple y de si hay uno o varios elementos
   input de tipo file) */
$imagen_a_subir = recogerImagenACargar($_FILES);
$validador = new ImageMimeGeter();
$esvalida = $validador->isValid($imagen_a_subir);
/* En este punto me podrían decir que no estoy respetando la signatura 
   del método isValid() pero no es cierto, acepta que le sea pasado el
   array de información del archivo a subir como único parámetro */

// El método de validación delega en parent la tarea, invocando:
$done = parent::isValid($value, $file);

/* Entré a revisar el código de ZF3 y me encontré con que el proceso 
   avanza sin fallos hasta el momento marcado abajo (copio parcialmente
   los métodos de ZF3 involucrados ya que cabe esperar que quien brinde 
   ayuda conoce el framework) */
/**
 * NOTA: Este método se encuentra en la clase MimeType
 *
 * Defined by ZendValidatorValidatorInterface
 *
 * Returns true if the mimetype of the file matches the given ones. Also parts
 * of mimetypes can be checked. If you give for example "image" all image
 * mime types will be accepted like "image/gif", "image/jpeg" and so on.
 *
 * @param  string|array $value Real file to check for mimetype
 * @param  array        $file  File data from ZendFileTransferTransfer (optional)
 * @return bool
 */
public function isValid($value, $file = null)
{
    if (is_string($value) && is_array($file)) {
        // Legacy ZendTransfer API support
        $filename = $file['name'];
        $filetype = $file['type'];
        $file     = $file['tmp_name'];
    } elseif (is_array($value)) {
        if (!isset($value['tmp_name']) || !isset($value['name']) || !isset($value['type'])) {
            throw new ExceptionInvalidArgumentException(
                'Value array must be in $_FILES format'
            );
        }
        $file     = $value['tmp_name'];
        $filename = $value['name'];
        $filetype = $value['type'];
    } else {
        $file     = $value;
        $filename = basename($file);
        $filetype = null;
    }
    $this->setValue($filename);

    // Is file readable ?
    if (empty($file) || false === stream_resolve_include_path($file)) {
        $this->error(static::NOT_READABLE);
        return false;
    }

    // AQUÍ Aborta el proceso. Falla al ejecutar getMagicFile()
    $mimefile = $this->getMagicFile();
    // Dejo el resto del método porque ilustra un excelente tratamiento
    // de la validación de tipos mime (Razón por la que me gusta ZFx)
    if (class_exists('finfo', false)) {
        if (!$this->isMagicFileDisabled() && (!empty($mimefile) && empty($this->finfo))) {
            ErrorHandler::start(E_NOTICE|E_WARNING);
            $this->finfo = finfo_open(FILEINFO_MIME_TYPE, $mimefile);
            ErrorHandler::stop();
        }

        if (empty($this->finfo)) {
            ErrorHandler::start(E_NOTICE|E_WARNING);
            $this->finfo = finfo_open(FILEINFO_MIME_TYPE);
            ErrorHandler::stop();
        }

        $this->type = null;
        if (!empty($this->finfo)) {
            $this->type = finfo_file($this->finfo, $file);
        }
    }

    if (empty($this->type) && $this->getHeaderCheck()) {
        $this->type = $filetype;
    }

    if (empty($this->type)) {
        $this->error(static::NOT_DETECTED);
        return false;
    }

    $mimetype = $this->getMimeType(true);
    if (in_array($this->type, $mimetype)) {
        return true;
    }

    $types = explode('/', $this->type);
    $types = array_merge($types, explode('-', $this->type));
    $types = array_merge($types, explode(';', $this->type));
    foreach ($mimetype as $mime) {
        if (in_array($mime, $types)) {
            return true;
        }
    }

    $this->error(static::FALSE_TYPE);
    return false;
}

Luego de identificar que el proceso avanza sin problemas hasta el punto señalado arriba getMagicFile() entré a revisar dicho procedimiento paso a paso e identifiqué el posible germen del mal:

/**
 * Returns the actual set magicfile
 *
 * @return string
 */
public function getMagicFile()
{
    if (null === $this->options['magicFile']) {
        $magic = getenv('magic');
        if (!empty($magic)) {
            // Si hubiese pasado por acá se habría caído como ocurre más
            // abajo
            $this->setMagicFile($magic);
            if ($this->options['magicFile'] === null) {
                $this->options['magicFile'] = false;
            }
            return $this->options['magicFile'];
        }

        foreach ($this->magicFiles as $file) {
            try {
            // En este punto se cae a pesar del try que se presume
            // atrapa el error y permite seguir
                $this->setMagicFile($file);
            } catch (ExceptionExceptionInterface $e) {
                // suppressing errors which are thrown due to open_basedir restrictions
                continue;
            }

            if ($this->options['magicFile'] !== null) {
                return $this->options['magicFile'];
            }
        }

        if ($this->options['magicFile'] === null) {
            $this->options['magicFile'] = false;
        }
    }

    return $this->options['magicFile'];
}

Antes de tratar de revisar qué pasa con el método setMagicFile($file), revisé el array de archivos mágicos recogidos por getenv() y sus paths para descartar problemas de privilegios de acceso. obtuve la siguiente lista:

    /usr/share/misc/magic
    /usr/share/misc/magic.mime
    /usr/share/misc/magic.mgc
    /usr/share/mime/magic
    /usr/share/mime/magic.mime
    /usr/share/mime/magic.mgc
    /usr/share/file/magic
    /usr/share/file/magic.mime
    /usr/share/file/magic.mgc

que como se puede observar están en directorios compartidos a los que el aplicativo tiene acceso.
Pasé, entonces a revisar el método setMagicFile($file) y me encontré que camina hasta invocar un método intrínseco de php: finfo_open()

Pase a consultar la documentación oficial de php (manual en español) y encontré un warning desagradable (que motiva esta farragosa consulta):

Advertencia
El formato esperado de la base de datos mágica cambió en PHP 5.3.11 y 5.4.1. Debido a esto, la base de datos mágica interna ha sido actualizada. Esto mayormente efectua código donde se use una base de datos mágica externa: leer un fichero mágico antiguo ahora fallará. Además, algunas representaciones textuales de los tipos mime han cambiado, por ejemplo, para PHP sería devuelto “PHP script, ASCII text” en lugar de “PHP script text”.

El servidor que empleo para desarrollo tiene instalado Php 5.6 y el sistema es Ubuntu 18.04 que a su vez tiene instalado Php 7.2 De modo que me lleva a pensar que muy seguramente los fallos detectados provienen del uso del método finfo_open().

Hice una prueba aparte de funcionamiento del método en cuestión pasando uno a uno los elementos del array mime (anotado arriba) para tratar de provocar el fallo y me encontré que no derriba el sistema en ningún caso, aunque falla en la mayoría, usé el siguiente código:

<?php
    $mimefiles = [
        '/usr/share/misc/magic',
        '/usr/share/misc/magic.mime',
        '/usr/share/misc/magic.mgc',
        '/usr/share/mime/magic',
        '/usr/share/mime/magic.mime',
        '/usr/share/mime/magic.mgc',
        '/usr/share/file/magic',
        '/usr/share/file/magic.mime',
        '/usr/share/file/magic.mgc'
    ];
    foreach ($mimefiles as $mf) {
        $finfo = finfo_open(FILEINFO_MIME_TYPE, $mf);
        if($finfo){
            mdf('Abierto '.$mf, 'warning');
        }else{
            mdf('FALLO al Abrir '.$mf, 'warning');
        }
        finfo_close($finfo);
    }
// mdf() es una función de mi API personal que me ofrece mensajes de
// depuración con formato (similar a un Dump pero con formato amigable)

y obtuve la siguiente salida:

Abierto /usr/share/misc/magic
FALLO al Abrir /usr/share/misc/magic.mime
FALLO al Abrir /usr/share/misc/magic.mgc
FALLO al Abrir /usr/share/mime/magic
FALLO al Abrir /usr/share/mime/magic.mime
FALLO al Abrir /usr/share/mime/magic.mgc
Abierto /usr/share/file/magic
FALLO al Abrir /usr/share/file/magic.mime
FALLO al Abrir /usr/share/file/magic.mgc

De modo que la hipótesis del fallo del método es falsa. En consecuencia, me encuentro en una encrucijada que me obliga a solicitar sugerencias. Si usted soportó todo el cuento, le estoy muy agradecido, si además puede ofrecerme una sugerencia razonable, tendré con usted una deuda inmensa. Gracias por “escucharme” leer.

Preguntado por: quevedo

Fuente

Tags:,

Add a Comment

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