Come controllare il tipo di file MIME con javascript prima del caricamento?


177

Ho letto questo e queste domande che sembrano suggerire che il tipo di file MIME possa essere verificato usando JavaScript sul lato client. Ora capisco che la vera convalida deve ancora essere eseguita sul lato server. Voglio eseguire un controllo lato client per evitare inutili sprechi di risorse del server.

Per verificare se ciò può essere fatto sul lato client, ho modificato l'estensione di un JPEGfile di test in .pnge ho scelto il file per il caricamento. Prima di inviare il file, eseguo una query sull'oggetto file utilizzando una console javascript:

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

Questo è ciò che ottengo su Chrome 28.0:

File {webkitRelativePath: "", lastModifiedDate: mar 16 ott 2012 10:00:00 GMT + 0000 (UTC), nome: "test.png", tipo: "image / png", dimensione: 500055 ...}

Mostra il tipo di essere image/pngche sembra indicare che il controllo viene eseguito in base all'estensione del file anziché al tipo MIME. Ho provato Firefox 22.0 e mi dà lo stesso risultato. Ma secondo le specifiche del W3C , MIME Sniffing dovrebbe essere implementato.

Ho ragione a dire che al momento non è possibile controllare il tipo MIME con javascript? Oppure mi sfugge qualcosa?


5
I want to perform a client side checking to avoid unnecessary wastage of server resource.Non capisco come mai tu dica che la convalida deve essere fatta sul lato server, ma poi dici che vuoi ridurre le risorse del server. Regola d'oro: non fidarti mai dell'input dell'utente . Qual è il punto di controllare il tipo MIME sul lato client se lo stai facendo solo sul lato server. Sicuramente si tratta di uno "spreco non necessario di risorse client "?
Ian Clark,

7
È consigliabile fornire una migliore verifica / feedback del tipo di file agli utenti lato client. Tuttavia, come hai affermato, i browser si basano semplicemente sulle estensioni dei file per determinare il valore della typeproprietà degli Fileoggetti. Il codice sorgente del webkit, ad esempio, rivela questa verità. È possibile identificare accuratamente i file sul lato client cercando, tra le altre cose, "byte magici" nei file. Attualmente sto lavorando a una biblioteca del MIT (in quel poco tempo libero che ho) che farà proprio questo. Se sei interessato ai miei progressi, dai un'occhiata a github.com/rnicholus/determinater .
Ray Nicholus,

32
@IanClark, il punto è che se il file è di un tipo non valido, posso rifiutarlo sul lato client piuttosto che sprecare la larghezza di banda di upload solo per rifiutarlo sul lato server.
Domanda Overflow

@RayNicholus, bello amico! Lo guarderò quando avrò il tempo. Grazie :)
Overflow della domanda il

Sei sicuro che il tuo file di test abbia ancora il mimetype image/jpege non l'hai modificato modificando l'estensione?
Bergi,

Risposte:


344

È possibile determinare facilmente il tipo MIME di file con JavaScript FileReaderprima di caricarlo su un server. Sono d'accordo che dovremmo preferire il controllo sul lato server sul lato client, ma il controllo sul lato client è ancora possibile. Ti mostrerò come e fornirò una demo funzionante in fondo.


Verifica che il tuo browser supporti sia Filee Blob. Tutti i principali dovrebbero.

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

Passo 1:

È possibile recuperare le Fileinformazioni da un <input>elemento come questo ( rif ):

<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>

Ecco una versione drag-and-drop di quanto sopra ( 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>

Passo 2:

Ora possiamo ispezionare i file e prendere in giro intestazioni e tipi MIME.

✘ Metodo rapido

Puoi ingenuamente chiedere a Blob il tipo MIME di qualunque file rappresenti usando questo modello:

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

Per le immagini, i tipi MIME tornano come segue:

image / jpeg
image / png
...

Avvertenza: il tipo MIME viene rilevato dall'estensione del file e può essere ingannato o falsificato. Si può rinominare a .jpgin a .pnge il tipo MIME verrà segnalato come image/png.


✓ Metodo di ispezione delle intestazioni corretto

Per ottenere il tipo MIME in buona fede di un file sul lato client possiamo fare un passo ulteriore e ispezionare i primi pochi byte del file dato per confrontare i cosiddetti numeri magici . Tieni presente che non è del tutto semplice perché, ad esempio, JPEG ha alcuni "numeri magici". Questo perché il formato si è evoluto dal 1991. Potresti cavartela controllando solo i primi due byte, ma preferisco controllare almeno 4 byte per ridurre i falsi positivi.

Firme di file di esempio di JPEG (primi 4 byte):

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

Ecco il codice essenziale per recuperare l'intestazione del file:

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);

È quindi possibile determinare il tipo MIME reale in questo modo (più firme di file qui e qui ):

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;
}

Accetta o rifiuta i caricamenti di file come preferisci in base ai tipi MIME previsti.


dimostrazione

Ecco una demo funzionante per file locali e file remoti (ho dovuto bypassare CORS solo per questa demo). Apri lo snippet, eseguilo e dovresti visualizzare tre immagini remote di diverso tipo visualizzate. Nella parte superiore è possibile selezionare un'immagine o un file di dati locali e verranno visualizzati la firma del file e / o il tipo MIME.

Si noti che anche se un'immagine viene rinominata, è possibile determinare il suo vero tipo MIME. Vedi sotto.

Immagine dello schermo

Uscita prevista della demo



8
2 commenti minori. (1) Non sarebbe meglio suddividere il file nei primi 4 byte prima della lettura? fileReader.readAsArrayBuffer(blob.slice(0,4))? (2) Per copiare / incollare le firme dei file, l'intestazione non dovrebbe essere costruita con 0 iniziali for(var i = 0; i < bytes.length; i++) { var byte = bytes[i]; fileSignature += (byte < 10 ? "0" : "") + byte.toString(16); }?
Matthew Madson,

1
@Deadpool Vedi qui . Esistono formati JPEG più, meno comuni, di diversi produttori. Ad esempio, FF D8 FF E2= CANNON EOS JPEG FILE, FF D8 FF E3= SAMSUNG D500 JPEG FILE. La parte chiave della firma JPEG è di soli 2 byte, ma per ridurre i falsi positivi ho aggiunto le firme a 4 byte più comuni. Spero che aiuti.
Drakes

24
La qualità di questa risposta è semplicemente incredibile.
Luca,

2
Non è necessario caricare il BLOB completo come ArrayBuffer per determinare il mimeType. Puoi semplicemente tagliare e passare i primi 4 byte del fileReader.readAsArrayBuffer(blob.slice(0, 4))
BLOB in

2
Quale dovrebbe essere il controllo per consentire solo testo normale? I primi 4 byte per i file di testo sembrano i primi 4 caratteri nel file di testo.
MP Droid,

19

Come indicato in altre risposte, puoi controllare il tipo mime controllando la firma del file nei primi byte del file.

Ma ciò che altre risposte stanno facendo è caricare l'intero file in memoria per verificare la firma, il che è molto dispendioso e potrebbe facilmente bloccare il browser se si seleziona un file di grandi dimensioni per errore o meno.

/**
 * 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>


Penso che readyStatesarà sempre FileReader.DONEnel gestore dell'evento ( specifica W3C ) anche se si fosse verificato un errore - il controllo non dovrebbe essere (!e.target.error)invece?
boycy,

5

Per chiunque stia cercando di non implementarlo da solo, Sindresorhus ha creato un'utilità che funziona nel browser e ha i mapping header-to-mime per la maggior parte dei documenti che potresti desiderare.

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

È possibile combinare il suggerimento di Vitim.us di leggere solo nei primi X byte per evitare di caricare tutto in memoria con l'utilizzo di questa utility (esempio in 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);

Per me, l'ultima versione della libreria non ha funzionato, ma ha "file-type": "12.4.0"funzionato e ho dovuto usarloimport * as fileType from "file-type";
SSZ

4

Se vuoi solo verificare se il file caricato è un'immagine, puoi semplicemente provare a caricarlo <img> tag e controllare se c'è errore nella richiamata.

Esempio:

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;
}

1
Funziona benissimo, ho provato un hacker di upload di file .gif e ha generato un errore :)
pathfinder,

4

Questo è ciò che devi fare

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

Se si desidera verificare i tipi di file immagine, quindi

if(fileVariable.type.match('image.*'))
{
 alert('its an image');
}

Attualmente non funziona per: Firefox per Android, Opera per Android e Safari su iOS. developer.mozilla.org/en-US/docs/Web/API/File/type
Reid

3

Ecco un'implementazione di Typescript che supporta webp. Questo si basa sulla risposta JavaScript di 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">


1

Come afferma Drake, ciò potrebbe essere fatto con FileReader. Tuttavia, ciò che presento qui è una versione funzionale. Prendi in considerazione che il grosso problema nel fare questo con JavaScript è resettare il file di input. Bene, questo si limita solo a JPG (per altri formati dovrai cambiare il tipo mime e il numero magico ):

<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>

Tieni presente che questo è stato testato sulle ultime versioni di Firefox e Chrome e su IExplore 10.

Per un elenco completo dei tipi di mime vedi Wikipedia .

Per un elenco completo dei numeri magici vedi Wikipedia .


I link di Wikipedia sopra non sono più validi.
Bob Quinn,

@BobQuinn risolto, grazie
lmiguelmh,

0

Ecco un'estensione della risposta di Roberto14 che procede come segue:

QUESTO CONSENTERÀ SOLO IMMAGINI

Verifica se FileReader è disponibile e ricade sull'estensione controllando se non è disponibile.

Fornisce un avviso di errore se non un'immagine

Se è un'immagine carica un'anteprima

** Dovresti comunque eseguire la convalida sul lato server, questo è più un vantaggio per l'utente finale che altro. Ma è utile!

<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>

-1

La risposta breve è no.

Come si nota, i browser derivano typedall'estensione del file. Anche l'anteprima per Mac sembra esaurirsi. Suppongo sia perché è più veloce nel leggere il nome del file contenuto nel puntatore, piuttosto che cercare e leggere il file sul disco.

Ho fatto una copia di un jpg rinominato con png.

Sono stato in grado di ottenere costantemente quanto segue da entrambe le immagini in Chrome (dovrebbe funzionare nei browser moderni).

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

Che potresti hackerare un controllo String.indexOf ('jpeg') per il tipo di immagine.

Ecco un violino per esplorare http://jsfiddle.net/bamboo/jkZ2v/1/

La linea ambigua che ho dimenticato di commentare nell'esempio

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

  • Divide i dati img codificati in base64, lasciando sull'immagine
  • Base64 decodifica l'immagine
  • Corrisponde solo alla prima riga dei dati dell'immagine

Il codice del violino utilizza la decodifica base64 che non funzionerà in IE9, ho trovato un buon esempio usando lo script VB che funziona in Internet Explorer http://blog.nihilogic.dk/2008/08/imageinfo-reading-image-metadata-with.html

Il codice per caricare l'immagine è stato preso da Joel Vardy, che sta facendo delle fantastiche immagini ridimensionando il lato client prima del caricamento, il che potrebbe essere di interesse https://joelvardy.com/writing/javascript-image-upload


1
Per favore non cercare JPEG per la sottostringa "jpeg", è solo una coincidenza che l'hai trovata in un commento. I file JPEG non devono contenerlo (e se invece stai pensando di cercarlo JFIF, APP0non devi contenere JFIF in EXIF-JPEG, quindi è anche disponibile).
Kornel,

Vedi in alto "La risposta breve è no".
Lex,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.