Asegurar un File Upload comprobando la firma de los archivos

Tenemos un simple formulario para elegir un fichero y subirlo al servidor y una pequeña rutina en PHP para verificar que el fichero es del tipo correcto (una imagen gif, jpeg o png) comprobando el Content-type enviado en la cabecera:

<?php
$valid_mime_types = array(
 "image/gif",
 "image/png",
 "image/jpeg",
 "image/pjpeg",
);

if(isset($_POST['upload']))
{
 $destination = 'uploads/' . $_FILES['userfile']['name'];
 if (in_array($_FILES['userfile']['type'], $valid_mime_types))
    {
  move_uploaded_file($_FILES['userfile']['tmp_name'], $destination);
  echo "<div>Upload succesful: <a href='$destination'>here</a><a href=\"\">×</a></div>";
    }
 else
 {
  echo "<div>>Upload failed.<a href=\"\">×</a></div>";
 }
}
?>

<form enctype="multipart/form-data" action="index.php" method="POST">
<fieldset>
 <legend>Upload</legend>
 <input name="userfile" class="small button" type="file" />
 <input type="submit" class="small button" name="upload" id="upload" value="Upload" />
</fieldset>
</form>

La linea if (in_array($_FILES['userfile']['type'], $valid_mime_types)) comprueba si el Content-type es alguno de los que aceptamos y, si la respuesta es positiva, procedemos a cargar el archivo en nuestro servidor y mostramos un mensaje de OK al usuario con un enlace al fichero cargado. El problema a resolver es cómo evitar que nos cuelen archivos de otro tipo falsificando el Content-type. La solución que muestro aquí puede ser útil cuando admitimos poca diversidad de tipos como en el caso del ejemplo (sólo gif, jpeg y png). Como sabemos, la mayoría de archivos de datos estandarizados contienen una lista de bytes en un orden determinado en alguna posición fija dentro del archivo: su firma. Se trata de añadir una función PHP que, después de cargar el fichero, compruebe ésta. Si se corresponde con los tipos admitidos se muestra el mensaje de archivo subido al usuario, en caso contrario se muestra error y se elimina el fichero:
function verificaArchivo($archivo)
{
// Matriz con los datos de cada tipo de archivo:
//     offsett al inicio del fichero
//     número de bytes
//     firma en hexadecimal

 $image_data = array (
  "jpeg" => array (
          "offsett" => 0,
   "lon" => 4,
   "firma" => "FFD8FFE0",
  ),
  "gif" => array (
   "offsett" => 0,
   "lon" => 3,
   "firma" => "474946",
  ),
  "png" => array (
   "offsett" => 0,
   "lon" => 4,
   "firma" => "89504E47",
  ),
 );
     
 $tipos = array_keys($image_data);
 $ok = false;
 do
 {
  if (!is_null($tipo = array_pop($tipos)))
// Lectura de los bytes necesarios del archivo cargado
   if ($datos = file_get_contents($archivo, NULL, NULL, $image_data[$tipo]["offsett"], $image_data[$tipo]["lon"]))
   {
    $hex = '';
// Convertimos los bytes obtenidos a hexadecimal
    for ($i = 0; $i < strlen($datos); $i++)
     $hex .= strtoupper(dechex(ord($datos[$i])));
// Y comparamos
    $ok = ($hex == $image_data[$tipo]["firma"]);
   }
 }
 while (!empty($tipos) && !$ok);
 return ($ok);     // Devuelve TRUE si se encontró el tipo
}
La función dispone de los datos de cada firma según el tipo en la matriz $image_data que se recorre para comparar la firma del fichero cargado con la de la matriz hasta encontrar una coincidencia, en caso de no encontrarla el fichero es rechazado. Modificamos pues la rutina original para llamar a esta función después de haber comprobado el Content-type y de haber subido el archivo;
if (in_array($_FILES['userfile']['type'], $valid_mime_types))
{
     move_uploaded_file($_FILES['userfile']['tmp_name'], $destination);
     if (verificaArchivo($destination))
          echo "<div>Upload succesful: <a href='$destination'>here</a></div>";
     else
     {
          echo "<div>Upload failed.</div>";
          unlink($destination);     // Firma no encontrada, se elimina el archivo
     }
}
Y ya disponemos de otro nivel de seguridad para nuestro formulario. Para las firmas de los tipos de archivos (signatures) puede consultarse la Wikipedia. Las firmas usadas en el ejemplo para los ficheros de imagen pueden cambiar dependiendo del software de tratamiento de imágenes que se use. Aún así no olvidemos que se puede insertar código malicioso en un JPG.

Fuente original del formulario: OWASP Bricks.
Enlaces: Malware oculto en cabeceras JPEG EXIF

No hay comentarios:

Publicar un comentario