¿Cómo verificar el tipo de archivo MIME con javascript antes de cargar?

177

He leído esto y estas preguntas que parecen sugerir que el tipo de archivo MIME podría verificarse usando javascript en el lado del cliente. Ahora, entiendo que la validación real todavía tiene que hacerse en el lado del servidor. Quiero realizar una verificación del lado del cliente para evitar el desperdicio innecesario de recursos del servidor.

Para probar si esto se puede hacer en el lado del cliente, cambié la extensión de un JPEGarchivo de prueba .pngy elegí el archivo para cargar. Antes de enviar el archivo, consulto el objeto del archivo usando una consola javascript:

document.getElementsByTagName('input')[0].files[0];

Esto es lo que obtengo en Chrome 28.0:

Archivo {webkitRelativePath: "", lastModifiedDate: Tue Oct 16 2012 10:00:00 GMT + 0000 (UTC), nombre: "test.png", tipo: "image / png", tamaño: 500055…}

Muestra el tipo de ser image/pngque parece indicar que la verificación se realiza en función de la extensión del archivo en lugar del tipo MIME. Probé Firefox 22.0 y me da el mismo resultado. Pero de acuerdo con las especificaciones del W3C , se debe implementar MIME Sniffing .

¿Tengo razón al decir que no hay forma de verificar el tipo MIME con javascript en este momento? ¿O me estoy perdiendo algo?

Desbordamiento de preguntas
fuente
55
I want to perform a client side checking to avoid unnecessary wastage of server resource.No entiendo por qué dice que la validación debe hacerse en el lado del servidor, pero luego dice que desea reducir los recursos del servidor. Regla de oro: nunca confíes en la entrada del usuario . ¿Cuál es el punto de verificar el tipo MIME en el lado del cliente si lo está haciendo en el lado del servidor? ¿Seguramente eso es un "desperdicio innecesario de recursos del cliente "?
Ian Clark
77
Proporcionar una mejor verificación / retroalimentación del tipo de archivo a los usuarios del lado del cliente es una buena idea. Sin embargo, como ha dicho, los navegadores simplemente confían en las extensiones de archivo al determinar el valor de la typepropiedad para los Fileobjetos. El código fuente del webkit, por ejemplo, revela esta verdad. Es posible identificar con precisión los archivos del lado del cliente buscando "bytes mágicos" en los archivos, entre otras cosas. Actualmente estoy trabajando en una biblioteca MIT (en el poco tiempo libre que tengo) que hará exactamente eso. Si está interesado en mi progreso, eche un vistazo a github.com/rnicholus/determinater .
Ray Nicholus
32
@IanClark, el punto es que si el archivo es de un tipo no válido, puedo rechazarlo en el lado del cliente en lugar de desperdiciar el ancho de banda de carga solo para rechazarlo en el lado del servidor.
Desbordamiento de preguntas
@RayNicholus, genial amigo! Lo miraré cuando tenga tiempo. Gracias :)
Desbordamiento de preguntas
¿Está seguro de que su archivo de prueba todavía tiene el tipo MIME image/jpeg, y en realidad no lo modificó cambiando la extensión?
Bergi

Respuestas:

342

Puede determinar fácilmente el tipo de archivo MIME con JavaScript FileReaderantes de cargarlo en un servidor. Estoy de acuerdo en que deberíamos preferir la verificación del lado del servidor sobre el lado del cliente, pero la verificación del lado del cliente todavía es posible. Le mostraré cómo y proporcionaré una demostración funcional en la parte inferior.


Verifique que su navegador sea compatible con ambos Filey Blob. Todos los principales deberían.

if (window.FileReader && window.Blob) {
    // All the File APIs are supported.
} else {
    // File and Blob are not supported
}

Paso 1:

Puede recuperar la Fileinformación de un <input>elemento como este ( ref ):

<input type="file" id="your-files" multiple>
<script>
var control = document.getElementById("your-files");
control.addEventListener("change", function(event) {
    // When the control has changed, there are new files
    var files = control.files,
    for (var i = 0; i < files.length; i++) {
        console.log("Filename: " + files[i].name);
        console.log("Type: " + files[i].type);
        console.log("Size: " + files[i].size + " bytes");
    }
}, false);
</script>

Aquí hay una versión de arrastrar y soltar de lo anterior ( ref ):

<div id="your-files"></div>
<script>
var target = document.getElementById("your-files");
target.addEventListener("dragover", function(event) {
    event.preventDefault();
}, false);

target.addEventListener("drop", function(event) {
    // Cancel default actions
    event.preventDefault();
    var files = event.dataTransfer.files,
    for (var i = 0; i < files.length; i++) {
        console.log("Filename: " + files[i].name);
        console.log("Type: " + files[i].type);
        console.log("Size: " + files[i].size + " bytes");
    }
}, false);
</script>

Paso 2:

Ahora podemos inspeccionar los archivos y extraer encabezados y tipos MIME.

✘ Método rápido

Puedes ingenuamente pedirle a Blob el tipo MIME de cualquier archivo que represente usando este patrón:

var blob = files[i]; // See step 1 above
console.log(blob.type);

Para las imágenes, los tipos MIME regresan de la siguiente manera:

image / jpeg
image / png
...

Advertencia: el tipo MIME se detecta desde la extensión del archivo y se puede engañar o suplantar. Se puede cambiar el nombre de a .jpga a .pngy el tipo MIME se informará como image/png.


✓ Método de inspección de encabezado adecuado

Para obtener el tipo MIME de buena fe de un archivo del lado del cliente, podemos ir un paso más allá e inspeccionar los primeros bytes del archivo dado para compararlos con los llamados números mágicos . Tenga en cuenta que no es del todo sencillo porque, por ejemplo, JPEG tiene algunos "números mágicos". Esto se debe a que el formato ha evolucionado desde 1991. Puede salirse con la suya al verificar solo los dos primeros bytes, pero prefiero verificar al menos 4 bytes para reducir los falsos positivos.

Ejemplo de firmas de archivo de JPEG (primeros 4 bytes):

FF D8 FF E0 (SOI + ADD0)
FF D8 FF E1 (SOI + ADD1)
FF D8 FF E2 (SOI + ADD2)

Aquí está el código esencial para recuperar el encabezado del archivo:

var blob = files[i]; // See step 1 above
var fileReader = new FileReader();
fileReader.onloadend = function(e) {
  var arr = (new Uint8Array(e.target.result)).subarray(0, 4);
  var header = "";
  for(var i = 0; i < arr.length; i++) {
     header += arr[i].toString(16);
  }
  console.log(header);

  // Check the file signature against known types

};
fileReader.readAsArrayBuffer(blob);

Luego puede determinar el tipo MIME real de esta manera (más firmas de archivos aquí y aquí ):

switch (header) {
    case "89504e47":
        type = "image/png";
        break;
    case "47494638":
        type = "image/gif";
        break;
    case "ffd8ffe0":
    case "ffd8ffe1":
    case "ffd8ffe2":
    case "ffd8ffe3":
    case "ffd8ffe8":
        type = "image/jpeg";
        break;
    default:
        type = "unknown"; // Or you can use the blob.type as fallback
        break;
}

Acepte o rechace las cargas de archivos como desee según los tipos MIME esperados.


Manifestación

Aquí hay una demostración funcional para archivos locales y archivos remotos (tuve que omitir CORS solo para esta demostración). Abra el fragmento, ejecútelo, y debería ver tres imágenes remotas de diferentes tipos mostradas. En la parte superior, puede seleccionar una imagen local o archivo de datos, y se mostrará la firma del archivo y / o el tipo MIME.

Tenga en cuenta que incluso si se cambia el nombre de una imagen, se puede determinar su verdadero tipo MIME. Vea abajo.

Captura de pantalla

Salida esperada de la demostración


Drakes
fuente
8
2 comentarios menores. (1) ¿No sería mejor dividir el archivo en sus primeros 4 bytes antes de la lectura? fileReader.readAsArrayBuffer(blob.slice(0,4))? (2) Para copiar / pegar firmas de archivos, ¿no debería construirse el encabezado con ceros a la izquierda for(var i = 0; i < bytes.length; i++) { var byte = bytes[i]; fileSignature += (byte < 10 ? "0" : "") + byte.toString(16); }?
Matthew Madson
1
@Deadpool Ver aquí . Hay más, menos comunes, formatos JPEG de diferentes fabricantes. Por ejemplo, FF D8 FF E2= ARCHIVO JPEG CANOS EOS, FF D8 FF E3= ARCHIVO JPEG SAMSUNG D500. La parte clave de la firma JPEG es de solo 2 bytes, pero para reducir los falsos positivos, agregué las firmas de 4 bytes más comunes. Espero que eso ayude.
Drakes el
23
La calidad de esta respuesta es simplemente increíble.
Luca
2
No tiene que cargar el blob completo como ArrayBuffer para determinar el mimeType. Puede simplemente cortar y pasar los primeros 4 bytes del blob de esta manera:fileReader.readAsArrayBuffer(blob.slice(0, 4))
codeVerine
2
¿Cuál debería ser la comprobación para permitir solo texto sin formato? Los primeros 4 bytes para los archivos de texto parecen ser los primeros 4 caracteres del archivo de texto.
MP Droid
19

Como se indicó en otras respuestas, puede verificar el tipo mime al verificar la firma del archivo en los primeros bytes del archivo.

Pero lo que están haciendo otras respuestas es cargar todo el archivo en la memoria para verificar la firma, lo cual es muy derrochador y podría congelar fácilmente su navegador si selecciona un archivo grande por accidente o no.

/**
 * Load the mime type based on the signature of the first bytes of the file
 * @param  {File}   file        A instance of File
 * @param  {Function} callback  Callback with the result
 * @author Victor www.vitim.us
 * @date   2017-03-23
 */
function loadMime(file, callback) {
    
    //List of known mimes
    var mimes = [
        {
            mime: 'image/jpeg',
            pattern: [0xFF, 0xD8, 0xFF],
            mask: [0xFF, 0xFF, 0xFF],
        },
        {
            mime: 'image/png',
            pattern: [0x89, 0x50, 0x4E, 0x47],
            mask: [0xFF, 0xFF, 0xFF, 0xFF],
        }
        // you can expand this list @see https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
    ];

    function check(bytes, mime) {
        for (var i = 0, l = mime.mask.length; i < l; ++i) {
            if ((bytes[i] & mime.mask[i]) - mime.pattern[i] !== 0) {
                return false;
            }
        }
        return true;
    }

    var blob = file.slice(0, 4); //read the first 4 bytes of the file

    var reader = new FileReader();
    reader.onloadend = function(e) {
        if (e.target.readyState === FileReader.DONE) {
            var bytes = new Uint8Array(e.target.result);

            for (var i=0, l = mimes.length; i<l; ++i) {
                if (check(bytes, mimes[i])) return callback("Mime: " + mimes[i].mime + " <br> Browser:" + file.type);
            }

            return callback("Mime: unknown <br> Browser:" + file.type);
        }
    };
    reader.readAsArrayBuffer(blob);
}


//when selecting a file on the input
fileInput.onchange = function() {
    loadMime(fileInput.files[0], function(mime) {

        //print the output to the screen
        output.innerHTML = mime;
    });
};
<input type="file" id="fileInput">
<div id="output"></div>

Vitim.us
fuente
Creo readyStateque siempre estará FileReader.DONEen el controlador de eventos ( especificación W3C ) incluso si hubo un error, ¿no debería ser la verificación si (!e.target.error)en su lugar?
boycy
5

Para cualquiera que esté buscando no implementar esto ellos mismos, Sindresorhus ha creado una utilidad que funciona en el navegador y tiene las asignaciones de encabezado a mimo para la mayoría de los documentos que pueda desear.

https://github.com/sindresorhus/file-type

Puede combinar la sugerencia de Vitim.us de solo leer en los primeros X bytes para evitar cargar todo en la memoria con el uso de esta utilidad (ejemplo en es6):

import fileType from 'file-type'; // or wherever you load the dependency

const blob = file.slice(0, fileType.minimumBytes);

const reader = new FileReader();
reader.onloadend = function(e) {
  if (e.target.readyState !== FileReader.DONE) {
    return;
  }

  const bytes = new Uint8Array(e.target.result);
  const { ext, mime } = fileType.fromBuffer(bytes);

  // ext is the desired extension and mime is the mimetype
};
reader.readAsArrayBuffer(blob);
Vinay
fuente
Para mí, la última versión de la biblioteca no funcionó, pero "file-type": "12.4.0"funcionó y tuve que usarimport * as fileType from "file-type";
ssz
4

Si solo desea verificar si el archivo cargado es una imagen, puede intentar cargarlo en la <img>etiqueta y verificar si hay alguna devolución de llamada de error.

Ejemplo:

var input = document.getElementsByTagName('input')[0];
var reader = new FileReader();

reader.onload = function (e) {
    imageExists(e.target.result, function(exists){
        if (exists) {

            // Do something with the image file.. 

        } else {

            // different file format

        }
    });
};

reader.readAsDataURL(input.files[0]);


function imageExists(url, callback) {
    var img = new Image();
    img.onload = function() { callback(true); };
    img.onerror = function() { callback(false); };
    img.src = url;
}
Roberto14
fuente
1
Funciona muy bien, probé un truco para cargar archivos .gif y arrojó un error :)
pathfinder
4

Esto es lo que tienes que hacer

var fileVariable =document.getElementsById('fileId').files[0];

Si desea verificar los tipos de archivos de imagen, entonces

if(fileVariable.type.match('image.*'))
{
 alert('its an image');
}
Kailas
fuente
Actualmente no funciona para: Firefox para Android, Opera para Android y Safari en iOS. developer.mozilla.org/en-US/docs/Web/API/File/type
Reid
3

Aquí hay una implementación de Typecript que admite webp. Esto se basa en la respuesta de JavaScript de Vitim.us.

interface Mime {
  mime: string;
  pattern: (number | undefined)[];
}

// tslint:disable number-literal-format
// tslint:disable no-magic-numbers
const imageMimes: Mime[] = [
  {
    mime: 'image/png',
    pattern: [0x89, 0x50, 0x4e, 0x47]
  },
  {
    mime: 'image/jpeg',
    pattern: [0xff, 0xd8, 0xff]
  },
  {
    mime: 'image/gif',
    pattern: [0x47, 0x49, 0x46, 0x38]
  },
  {
    mime: 'image/webp',
    pattern: [0x52, 0x49, 0x46, 0x46, undefined, undefined, undefined, undefined, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50],
  }
  // You can expand this list @see https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
];
// tslint:enable no-magic-numbers
// tslint:enable number-literal-format

function isMime(bytes: Uint8Array, mime: Mime): boolean {
  return mime.pattern.every((p, i) => !p || bytes[i] === p);
}

function validateImageMimeType(file: File, callback: (b: boolean) => void) {
  const numBytesNeeded = Math.max(...imageMimes.map(m => m.pattern.length));
  const blob = file.slice(0, numBytesNeeded); // Read the needed bytes of the file

  const fileReader = new FileReader();

  fileReader.onloadend = e => {
    if (!e || !fileReader.result) return;

    const bytes = new Uint8Array(fileReader.result as ArrayBuffer);

    const valid = imageMimes.some(mime => isMime(bytes, mime));

    callback(valid);
  };

  fileReader.readAsArrayBuffer(blob);
}

// When selecting a file on the input
fileInput.onchange = () => {
  const file = fileInput.files && fileInput.files[0];
  if (!file) return;

  validateImageMimeType(file, valid => {
    if (!valid) {
      alert('Not a valid image file.');
    }
  });
};

<input type="file" id="fileInput">

Eric Coulthard
fuente
1

Como dice Drake, esto podría hacerse con FileReader. Sin embargo, lo que presento aquí es una versión funcional. Tenga en cuenta que el gran problema al hacer esto con JavaScript es restablecer el archivo de entrada. Bueno, esto se limita solo a JPG (para otros formatos, tendrá que cambiar el tipo MIME y el número mágico ):

<form id="form-id">
  <input type="file" id="input-id" accept="image/jpeg"/>
</form>

<script type="text/javascript">
    $(function(){
        $("#input-id").on('change', function(event) {
            var file = event.target.files[0];
            if(file.size>=2*1024*1024) {
                alert("JPG images of maximum 2MB");
                $("#form-id").get(0).reset(); //the tricky part is to "empty" the input file here I reset the form.
                return;
            }

            if(!file.type.match('image/jp.*')) {
                alert("only JPG images");
                $("#form-id").get(0).reset(); //the tricky part is to "empty" the input file here I reset the form.
                return;
            }

            var fileReader = new FileReader();
            fileReader.onload = function(e) {
                var int32View = new Uint8Array(e.target.result);
                //verify the magic number
                // for JPG is 0xFF 0xD8 0xFF 0xE0 (see https://en.wikipedia.org/wiki/List_of_file_signatures)
                if(int32View.length>4 && int32View[0]==0xFF && int32View[1]==0xD8 && int32View[2]==0xFF && int32View[3]==0xE0) {
                    alert("ok!");
                } else {
                    alert("only valid JPG images");
                    $("#form-id").get(0).reset(); //the tricky part is to "empty" the input file here I reset the form.
                    return;
                }
            };
            fileReader.readAsArrayBuffer(file);
        });
    });
</script>

Tenga en cuenta que esto se probó en las últimas versiones de Firefox y Chrome, y en IExplore 10.

Para obtener una lista completa de los tipos de mimos, consulte Wikipedia .

Para obtener una lista completa del número mágico, consulte Wikipedia .

lmiguelmh
fuente
Los enlaces de Wikipedia anteriores ya no son válidos.
Bob Quinn
@BobQuinn arreglado, thansk
lmiguelmh
0

Aquí hay una extensión de la respuesta de Roberto14 que hace lo siguiente:

ESTO SOLO PERMITIRÁ IMÁGENES

Comprueba si FileReader está disponible y vuelve a la extensión comprobando si no está disponible.

Da una alerta de error si no es una imagen

Si es una imagen, carga una vista previa

** Aún debe hacer la validación del lado del servidor, esto es más una conveniencia para el usuario final que cualquier otra cosa. ¡Pero es útil!

<form id="myform">
    <input type="file" id="myimage" onchange="readURL(this)" />
    <img id="preview" src="#" alt="Image Preview" />
</form>

<script>
function readURL(input) {
    if (window.FileReader && window.Blob) {
        if (input.files && input.files[0]) {
            var reader = new FileReader();
            reader.onload = function (e) {
                var img = new Image();
                img.onload = function() {
                    var preview = document.getElementById('preview');
                    preview.src = e.target.result;
                    };
                img.onerror = function() { 
                    alert('error');
                    input.value = '';
                    };
                img.src = e.target.result;
                }
            reader.readAsDataURL(input.files[0]);
            }
        }
    else {
        var ext = input.value.split('.');
        ext = ext[ext.length-1].toLowerCase();      
        var arrayExtensions = ['jpg' , 'jpeg', 'png', 'bmp', 'gif'];
        if (arrayExtensions.lastIndexOf(ext) == -1) {
            alert('error');
            input.value = '';
            }
        else {
            var preview = document.getElementById('preview');
            preview.setAttribute('alt', 'Browser does not support preview.');
            }
        }
    }
</script>
pionero
fuente
-1

La respuesta corta es no.

Como observa, los navegadores derivan typede la extensión del archivo. La vista previa de Mac también parece salir de la extensión. Supongo que es porque es más rápido leer el nombre del archivo contenido en el puntero, en lugar de buscar y leer el archivo en el disco.

Hice una copia de un jpg renombrado con png.

Pude obtener constantemente lo siguiente de ambas imágenes en Chrome (debería funcionar en los navegadores modernos).

ÿØÿàJFIFÿþ;CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), quality = 90

Que podría hackear una comprobación String.indexOf ('jpeg') para el tipo de imagen.

Aquí hay un violín para explorar http://jsfiddle.net/bamboo/jkZ2v/1/

La línea ambigua que olvidé comentar en el ejemplo

console.log( /^(.*)$/m.exec(window.atob( image.src.split(',')[1] )) );

  • Divide los datos img codificados en base64, dejando en la imagen
  • Base64 decodifica la imagen
  • Coincide solo con la primera línea de los datos de la imagen

El código de violín usa decodificación base64 que no funcionará en IE9, encontré un buen ejemplo usando el script VB que funciona en IE http://blog.nihilogic.dk/2008/08/imageinfo-reading-image-metadata-with.html

El código para cargar la imagen fue tomado de Joel Vardy, quien está haciendo un lienzo de imagen genial cambiando el tamaño del lado del cliente antes de cargarlo, lo que puede ser de interés https://joelvardy.com/writing/javascript-image-upload

Lex
fuente
1
No busque en JPEG la subcadena "jpeg", es solo una coincidencia que la haya encontrado en un comentario. Los archivos JPEG no tienen que contenerlo (y si estás pensando en buscarlo JFIF, bueno APP0, no tiene que contener JFIF en EXIF-JPEG, así que eso también está fuera).
Kornel
Ver arriba "La respuesta corta es no".
Lex