miércoles, 20 de junio de 2012

Validar el formato de una imagen (no me mientas con el formato) con C#

De verdad que hay veces en la que es mejor estarse callados porque si hablas puedes abrir La caja de Pandora y al final terminarás muy pero que muy mal.

Toda esta historia comienza con un simple FileUpload que permite al usuario subir una imagen. En principio esto no debería dar ningún problema, es algo que se encuentra perfectamente documento en Internet y en menos de 2 minutos lo tienes montado en tu aplicación. El problema surge cuando a alguien se le ocurre hacer la siguiente pregunta, ¿qué pasa si no suben una imagen, o sea, si algún avispado coge un ejecutable y le cambia la extensión a por ejemplo, jpg? La verdad es que mis conocimientos sobre seguridad no llegan a tanto y no se cuales serías las implicaciones reales sobre tal acción pero con semejante pregunta ya tenemos el lío montado así que, toca hacer un sistema para comprobar que la imagen que el usuario ha subido es realmente la imagen que dice ser.

Lo primero es preguntarnos cuales son los formatos gráficos que vamos a admitir, lo cual, es una mala pregunta porque la respuesta fue, todos. Eso es imposible así que, vamos a limitarlo a 3 o 4, así que la cosa quedó en los más utilizados, esto es, jpeg, gif, png y bmp.

Para esto podríamos hacer uso del objeto Image que nos da .NET, pero alguien (y no soy yo) se le metieron en la cabeza estás dos cosas
  • Usar el objeto Image es poco eficiente.
  • Referenciar a System.Drawing en la capa de negocio es poco elegante.
De todas formas aquí está como podría ser este código (por si alguien quiere usarlo)
public bool validateImage(byte[] bytes)
{
  try 
  {
    Stream stream = new MemoryStream(bytes);
    using(Image img = Image.FromStream(stream))
    {
      if (img.RawFormat.Equals(ImageFormat.Bmp) ||
          img.RawFormat.Equals(ImageFormat.Gif) ||
          img.RawFormat.Equals(ImageFormat.Jpeg) ||
          img.RawFormat.Equals(ImageFormat.Png))
        return true;
    }
    return false;
  } 
  catch
  {
    return false;
  }
}
Tras revisar un poco de documentación sobre estos formatos (la wikipedia es un lugar fantástico) llegamos a esta conclusión.
  • JPEG: Los primeros 4 bytes son FF D8 FF E0 (aunque parece que lo realmente seguro son los dos primeros bytes, ya que por ejemplo algunas cámaras Canon ponen FF E1 en el tercer y cuatro byte).
  • GIF: Los primeros 6 bytes son siempre "GIF87a" o "GIF89a".
  • PNG: Los primeros 8 bytes son 89 50 4E 47 0D 0A 1A 0A.
  • BMP: Los primeros 2 bytes son 42 4D.
Con esta información ya nos podemos poner manos a la obra. Para realizar las validaciones implementamos una pequeña factoría de validadores de imágenes ya que después de esto vinieron más cosas, y al final lo mejor fue encapsular todo el comportamiento específico de cada imagen en un clase (como se debería hacer siempre, ¿no?).
Nuestra interface para la validación del formato
public interface IImageValidator
{
  byte[] Content { get; set; }
  bool IsValid();
}
Nuestra factoría de interfaces
public static IImageValidator Create(string extension, byte[] content)
{
  switch (extension.ToUpper())
  {
    case "BMP":
      return new BMPImageValidator() { Content = content };
    case "GIF":
      return new GIFImageValidator() { Content = content };
    case "JPG":
    case "JPEG":
      return new JPEGImageValidator() { Content = content };
    case "PNG":
      return new PNGImageValidator() { Content = content };
    default:
      throw new Exception(string.Format("Format '{0}' not supported", extension));
  }
}
Y por último como sería el validador para el caso de un fichero jpg
public class JPEGImageValidator : IImageValidator
{
  #region IImageValidator

  public byte[] Content { get; set; }

  public bool IsValid()
  {
    byte[] magicNumber = { 0xFF, 0xD8 };   
    return (Content.Take(2).ToArray() == magicNumber);
  }

  #endregion
}
Para usar el validador tan solo debemos hacer lo siguiente
byte[] imageJPG = System.IO.File.ReadAllBytes("test-jpg.jpg");
IImageValidator validator = ImageValidatorFactory.Create("jpg", imageJPG);
bool isValid = validator.IsValid();
Para la prueba estoy cargando la imagen desde un fichero. Si queremos usarlo en un entorno web (MVC) podemos hacer lo siguiente
string extension = string.Empty;
if (file.FileName.LastIndexOf('.') > 0)
  extension = file.FileName.Substring(file.FileName.LastIndexOf('.') + 1);       
MemoryStream memoryStream = new MemoryStream();
file.InputStream.CopyTo(memoryStream);
byte[] content = memoryStream.ToArray();

IImageValidator validator = ImageValidatorFactory.Create(extension, content);
bool isValid = validator.IsValid();
Respecto a la eficiencia de los métodos, he modificado el método validateImage para que en vez de validar un byte[] también valide un Stream, para ahorrarnos esta conversión y así ser más justos en la comparación. Los resultados son realmente demoledores

  • validateImage (byte[]): 5601 ms.
  • validateImage (stream): 5157 ms.
  • ImageValidatorFactory: 4 ms.

Así que realmente el uso de la clase Image es un factor a tener en cuenta en caso que trabajemos de manera masiva con imágenes.

Viendo estos números puede que quede justificado hacer nuestros propios validadores de imágenes aunque para otras tareas puede que no quede tan justificado sobre todo teniendo en cuenta los mil y un detalles que hay que tener en cuenta cuando trabajamos con este tipo de ficheros a bajo nivel.

Happy coding!

No hay comentarios:

Publicar un comentario