260 votes

Comment vérifier le type MIME d'un fichier avec javascript avant de le télécharger ?

J'ai lu este y este qui semble suggérer que le type MIME du fichier pourrait être vérifié en utilisant le javascript du côté client. Maintenant, je comprends que la véritable validation doit encore être effectuée côté serveur. Je veux effectuer une vérification côté client pour éviter de gaspiller inutilement les ressources du serveur.

Pour tester si cela peut être fait du côté client, j'ai modifié l'extension d'un fichier de type JPEG fichier de test vers .png et choisissez le fichier à télécharger. Avant d'envoyer le fichier, j'interroge l'objet fichier à l'aide d'une console javascript :

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

Voici ce que j'obtiens sur Chrome 28.0 :

Fichier {webkitRelativePath : "", lastModifiedDate : Tue Oct 16 2012 10:00:00 GMT+0000 (UTC), nom : "test.png", type : "image/png", taille : 500055 }

Il montre le type à être image/png ce qui semble indiquer que la vérification est effectuée en fonction de l'extension du fichier et non du type MIME. J'ai essayé Firefox 22.0 et il me donne le même résultat. Mais selon la spécification du W3C , Reniflage de MIME doit être mis en œuvre.

Ai-je raison de dire qu'il n'y a aucun moyen de vérifier le type MIME avec javascript pour le moment ? Ou est-ce que quelque chose m'échappe ?

6 votes

I want to perform a client side checking to avoid unnecessary wastage of server resource. Je ne comprends pas pourquoi vous dites que la validation doit être effectuée du côté du serveur, mais dites ensuite que vous voulez réduire les ressources du serveur. Règle d'or : Ne jamais faire confiance aux données de l'utilisateur . Quel est l'intérêt de vérifier le type MIME du côté client si vous le faites ensuite du côté serveur. Il s'agit certainement d'un "gaspillage inutile de ressources". client ressource" ?

10 votes

Fournir une meilleure vérification du type de fichier/un meilleur retour d'information aux utilisateurs côté client est une bonne idée. Cependant, comme vous l'avez indiqué, les navigateurs se basent simplement sur les extensions de fichier pour déterminer la valeur de la propriété type propriété pour File objets. Le code source de webkit, par exemple, révèle cette vérité. Il est possible d'identifier avec précision les fichiers côté client en recherchant, entre autres, des "octets magiques" dans les fichiers. Je travaille actuellement sur une bibliothèque MIT (dans le peu de temps libre dont je dispose) qui fera exactement cela. Si vous êtes intéressé par mes progrès, jetez un coup d'œil à github.com/rnicholus/determinater .

54 votes

@IanClark, le fait est que si le fichier est d'un type non valide, je peux le rejeter du côté client plutôt que de gaspiller la bande passante de téléchargement pour ensuite le rejeter du côté serveur.

504voto

Drakes Points 17066

Vous pouvez facilement déterminer le type MIME du fichier avec la fonction JavaScript FileReader avant de le télécharger sur un serveur. Je suis d'accord pour dire que nous devrions préférer la vérification côté serveur à la vérification côté client, mais la vérification côté client est toujours possible. Je vous montrerai comment faire et vous fournirai une démonstration fonctionnelle en bas de page.


Vérifiez que votre navigateur prend en charge les deux File y Blob . Toutes les grandes entreprises devraient le faire.

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

Étape 1 :

Vous pouvez récupérer le File des informations provenant d'un <input> comme ceci ( réf. ) :

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

Voici une version drag-and-drop de l'image ci-dessus ( réf. ) :

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

Étape 2 :

Nous pouvons maintenant inspecter les fichiers et en extraire les en-têtes et les types MIME.

✘ Méthode rapide

Vous pouvez demander naïvement Blob pour le type MIME du fichier qu'il représente en utilisant ce modèle :

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

Pour les images, les types MIME reviennent comme suit :

image/jpeg
image/png
...

Attention : Le type MIME est détecté à partir de l'extension du fichier et peut être trompé ou usurpé. On peut renommer un fichier .jpg à un .png et le type MIME sera signalé comme étant image/png .


✓ Méthode appropriée d'inspection des en-têtes

Pour obtenir le véritable type MIME d'un fichier côté client, nous pouvons aller un peu plus loin et inspecter les premiers octets du fichier donné pour le comparer à ce que l'on appelle chiffres magiques . Sachez que ce n'est pas tout à fait simple car, par exemple, JPEG a quelques "chiffres magiques". Cela s'explique par le fait que le format a évolué depuis 1991. Vous pouvez vous en sortir en ne vérifiant que les deux premiers octets, mais je préfère vérifier au moins 4 octets pour réduire les faux positifs.

Exemple de signatures de fichiers JPEG (4 premiers octets) :

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

Voici le code essentiel pour récupérer l'en-tête du fichier :

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

Vous pouvez alors déterminer le véritable type MIME comme suit (plus de signatures de fichiers) 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;
}

Acceptez ou refusez les téléchargements de fichiers comme vous le souhaitez en fonction des types MIME attendus.


Démo

Voici une démonstration fonctionnelle pour les fichiers locaux y des fichiers distants (j'ai dû contourner CORS juste pour cette démo). Ouvrez le snippet, exécutez-le, et vous devriez voir trois images distantes de différents types affichées. En haut, vous pouvez sélectionner une image locale o le fichier de données, et la signature du fichier et/ou le type MIME seront affichés.

Notez que même si une image est renommée, son véritable type MIME peut être déterminé. Voir ci-dessous.

Capture d'écran

Expected output of demo


// Return the first few bytes of the file as a hex string
function getBLOBFileHeader(url, blob, callback) {
  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);
    }
    callback(url, header);
  };
  fileReader.readAsArrayBuffer(blob);
}

function getRemoteFileHeader(url, callback) {
  var xhr = new XMLHttpRequest();
  // Bypass CORS for this demo - naughty, Drakes
  xhr.open('GET', '//cors-anywhere.herokuapp.com/' + url);
  xhr.responseType = "blob";
  xhr.onload = function() {
    callback(url, xhr.response);
  };
  xhr.onerror = function() {
    alert('A network error occurred!');
  };
  xhr.send();
}

function headerCallback(url, headerString) {
  printHeaderInfo(url, headerString);
}

function remoteCallback(url, blob) {
  printImage(blob);
  getBLOBFileHeader(url, blob, headerCallback);
}

function printImage(blob) {
  // Add this image to the document body for proof of GET success
  var fr = new FileReader();
  fr.onloadend = function() {
    $("hr").after($("<img>").attr("src", fr.result))
      .after($("<div>").text("Blob MIME type: " + blob.type));
  };
  fr.readAsDataURL(blob);
}

// Add more from http://en.wikipedia.org/wiki/List_of_file_signatures
function mimeType(headerString) {
  switch (headerString) {
    case "89504e47":
      type = "image/png";
      break;
    case "47494638":
      type = "image/gif";
      break;
    case "ffd8ffe0":
    case "ffd8ffe1":
    case "ffd8ffe2":
      type = "image/jpeg";
      break;
    default:
      type = "unknown";
      break;
  }
  return type;
}

function printHeaderInfo(url, headerString) {
  $("hr").after($("<div>").text("Real MIME type: " + mimeType(headerString)))
    .after($("<div>").text("File header: 0x" + headerString))
    .after($("<div>").text(url));
}

/* Demo driver code */

var imageURLsArray = ["http://media2.giphy.com/media/8KrhxtEsrdhD2/giphy.gif", "http://upload.wikimedia.org/wikipedia/commons/e/e9/Felis_silvestris_silvestris_small_gradual_decrease_of_quality.png", "http://static.giantbomb.com/uploads/scale_small/0/316/520157-apple_logo_dec07.jpg"];

// Check for FileReader support
if (window.FileReader && window.Blob) {
  // Load all the remote images from the urls array
  for (var i = 0; i < imageURLsArray.length; i++) {
    getRemoteFileHeader(imageURLsArray[i], remoteCallback);
  }

  /* Handle local files */
  $("input").on('change', function(event) {
    var file = event.target.files[0];
    if (file.size >= 2 * 1024 * 1024) {
      alert("File size must be at most 2MB");
      return;
    }
    remoteCallback(escape(file.name), file);
  });

} else {
  // File and Blob are not supported
  $("hr").after( $("<div>").text("It seems your browser doesn't support FileReader") );
} /* Drakes, 2015 */

img {
  max-height: 200px
}
div {
  height: 26px;
  font: Arial;
  font-size: 12pt
}
form {
  height: 40px;
}

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<form>
  <input type="file" />
  <div>Choose an image to see its file signature.</div>
</form>
<hr/>

10 votes

2 commentaires mineurs. (1) Ne serait-il pas préférable de découper le fichier en 4 octets avant de le lire ? fileReader.readAsArrayBuffer(blob.slice(0,4)) ? (2) Pour copier/coller des signatures de fichiers, l'en-tête ne devrait-il pas être construit avec des 0 de tête ? for(var i = 0; i < bytes.length; i++) { var byte = bytes[i]; fileSignature += (byte < 10 ? "0" : "") + byte.toString(16); } ?

0 votes

@drakes où avez-vous obtenu la signature du fichier ffd8ffe2 ? D'après le lien wiki que vous avez donné, les signatures de fichiers pour jpeg sont ffd8ffdb , ffd8ffe0 y ffd8ffe1 . Pouvez-vous confirmer ? Merci

1 votes

@Deadpool Voir aquí . Il existe d'autres formats JPEG, moins courants, provenant de différents fabricants. Par exemple, FF D8 FF E2 = CANON EOS FICHIER JPEG, FF D8 FF E3 = FICHIER JPEG SAMSUNG D500. La partie essentielle de la signature JPEG ne comporte que 2 octets, mais pour réduire les faux positifs, j'ai ajouté les signatures à 4 octets les plus courantes. J'espère que cela vous aidera.

33voto

Vitim.us Points 3340

Comme indiqué dans d'autres réponses, vous pouvez vérifier le type de mime en vérifiant le fichier signature du fichier dans les premiers octets du fichier.

Mais ce que les autres réponses font, c'est charger le fichier entier en mémoire afin de vérifier la signature, ce qui est très inutile et pourrait facilement geler votre navigateur si vous sélectionnez un gros fichier par accident ou non.

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

1 votes

Je pense readyState sera toujours FileReader.DONE dans le gestionnaire d'événements ( Spécification du W3C ) même s'il y avait une erreur - la vérification ne devrait-elle pas être si (!e.target.error) à la place ?

17voto

Vinay Points 2156

Pour tous ceux qui ne souhaitent pas mettre en œuvre ce système eux-mêmes, Sindresorhus a créé un utilitaire qui fonctionne dans le navigateur et qui fournit les correspondances en-tête-mime pour la plupart des documents que vous pouvez souhaiter.

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

Vous pourriez combiner la suggestion de Vitim.us de ne lire que les X premiers octets pour éviter de tout charger en mémoire avec l'utilisation de cet utilitaire (exemple dans 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);

3 votes

Pour moi, la dernière version de la bibliothèque n'a pas fonctionné mais la "file-type": "12.4.0" a fonctionné et j'ai dû utiliser import * as fileType from "file-type";

6voto

Eric Coulthard Points 342

Voici une implémentation de Typescript qui supporte webp. Elle est basée sur la réponse 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">

2 votes

La question demande une solution en javascript strictement. Bien que je comprenne que typescript est facilement convertible en javascript, c'est toujours une étape supplémentaire qui échappe aux limites de la question originale.

4voto

Roberto14 Points 535

Si vous voulez simplement vérifier si le fichier téléchargé est une image, vous pouvez essayer de le charger dans le fichier <img> et vérifier s'il y a un rappel d'erreur.

Exemple :

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 votes

Cela fonctionne très bien, j'ai essayé un hacker de téléchargement de fichier .gif et cela a donné une erreur :)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X