Jak sprawdzić typ MIME pliku za pomocą javascript przed przesłaniem?

177

Przeczytałem to i to pytanie, które wydaje się sugerować, że typ MIME pliku można sprawdzić za pomocą javascript po stronie klienta. Teraz rozumiem, że prawdziwa walidacja nadal musi zostać przeprowadzona po stronie serwera. Chcę przeprowadzić kontrolę po stronie klienta, aby uniknąć niepotrzebnego marnowania zasobów serwera.

Aby sprawdzić, czy można to zrobić po stronie klienta, zmieniłem rozszerzenie JPEGpliku testowego na .pngi wybrałem plik do przesłania. Przed wysłaniem pliku odpytuję obiekt pliku za pomocą konsoli javascript:

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

Oto, co otrzymuję w Chrome 28.0:

Plik {webkitRelativePath: "", lastModifiedDate: wt. 16 października 2012 10:00:00 GMT + 0000 (UTC), nazwa: "test.png", typ: "image / png", rozmiar: 500055…}

Pokazuje typ, image/pngktóry wydaje się wskazywać, że sprawdzanie odbywa się na podstawie rozszerzenia pliku zamiast typu MIME. Wypróbowałem Firefox 22.0 i daje mi ten sam wynik. Ale zgodnie ze specyfikacją W3C , MIME Sniffing powinno być zaimplementowane .

Czy mam rację mówiąc, że w tej chwili nie ma możliwości sprawdzenia typu MIME za pomocą javascript? A może coś mi brakuje?

Przepełnienie pytań
źródło
5
I want to perform a client side checking to avoid unnecessary wastage of server resource.Nie rozumiem, dlaczego mówisz, że walidacja musi być wykonywana po stronie serwera, ale potem mówisz, że chcesz zmniejszyć zasoby serwera. Złota zasada: nigdy nie ufaj wprowadzaniu danych przez użytkownika . Jaki jest sens sprawdzania typu MIME po stronie klienta, jeśli robisz to po prostu po stronie serwera. Z pewnością jest to „niepotrzebne marnotrawstwo zasobów klienta ”?
Ian Clark
7
Zapewnienie lepszej kontroli typów plików / informacji zwrotnych dla użytkowników po stronie klienta jest dobrym pomysłem. Jednak, jak już powiedziałeś, przeglądarki po prostu polegają na rozszerzeniach plików podczas określania wartości typewłaściwości Fileobiektów. Na przykład kod źródłowy webkita ujawnia tę prawdę. Możliwe jest dokładne zidentyfikowanie plików po stronie klienta, między innymi wyszukując w nich „magiczne bajty”. Obecnie pracuję nad biblioteką MIT (w jakim mam wolny czas), która właśnie to zrobi. Jeśli jesteś zainteresowany moimi postępami, zajrzyj na github.com/rnicholus/determinater .
Ray Nicholus
32
@IanClark, chodzi o to, że jeśli plik jest nieprawidłowego typu, mogę go odrzucić po stronie klienta, zamiast marnować przepustowość wysyłania tylko po to, aby odrzucić go po stronie serwera.
Question Overflow
@RayNicholus, fajny koleś! Przejrzę to, kiedy będę miał czas. Dzięki :)
Przepełnienie pytania
Czy jesteś pewien, że Twój plik testowy nadal ma typ MIME image/jpegi nie zmodyfikowałeś go, zmieniając rozszerzenie?
Bergi

Odpowiedzi:

343

Możesz łatwo określić typ MIME pliku za pomocą JavaScript FileReaderprzed przesłaniem go na serwer. Zgadzam się, że powinniśmy preferować sprawdzanie po stronie serwera niż po stronie klienta, ale sprawdzanie po stronie klienta jest nadal możliwe. Pokażę ci, jak i zapewnię działające demo na dole.


Sprawdź, czy Twoja przeglądarka obsługuje zarówno Filei Blob. Wszystkie najważniejsze powinny.

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

Krok 1:

Możesz pobrać Fileinformacje z <input>elementu takiego jak ten ( 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>

Oto wersja powyższego ( ref ) typu „przeciągnij i upuść” :

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

Krok 2:

Możemy teraz przeglądać pliki i usuwać nagłówki oraz typy MIME.

✘ Szybka metoda

Możesz naiwnie zapytać Bloba o typ MIME dowolnego pliku, który reprezentuje, używając tego wzorca:

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

W przypadku obrazów typy MIME powracają w następujący sposób:

image / jpeg
image / png
...

Uwaga: typ MIME jest wykrywany na podstawie rozszerzenia pliku i może zostać oszukany lub sfałszowany. Można zmienić nazwę a .jpgna a, .pnga typ MIME zostanie zgłoszony jako image/png.


✓ Właściwa metoda kontroli nagłówka

Aby uzyskać prawdziwy typ MIME pliku po stronie klienta, możemy pójść o krok dalej i sprawdzić kilka pierwszych bajtów danego pliku, aby porównać je z tak zwanymi liczbami magicznymi . Ostrzegamy, że nie jest to całkowicie proste, ponieważ na przykład JPEG ma kilka „magicznych liczb”. Dzieje się tak, ponieważ format ewoluował od 1991 roku. Możesz uciec od sprawdzania tylko pierwszych dwóch bajtów, ale ja wolę sprawdzić co najmniej 4 bajty, aby zredukować fałszywe alarmy.

Przykładowe podpisy plików JPEG (pierwsze 4 bajty):

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

Oto podstawowy kod do pobrania nagłówka pliku:

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

Następnie możesz określić rzeczywisty typ MIME w ten sposób (więcej sygnatur plików tutaj i tutaj ):

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

Akceptuj lub odrzucaj przesyłanie plików, jak chcesz, na podstawie oczekiwanych typów MIME.


Próbny

Oto działające demo dla plików lokalnych i plików zdalnych (musiałem ominąć CORS tylko dla tego demo). Otwórz fragment kodu, uruchom go i powinieneś zobaczyć trzy zdalne obrazy różnych typów. U góry możesz wybrać lokalny obraz lub plik danych, a zostanie wyświetlony podpis pliku i / lub typ MIME.

Zauważ, że nawet jeśli nazwa obrazu zostanie zmieniona, można określić jego prawdziwy typ MIME. Zobacz poniżej.

Zrzut ekranu

Oczekiwany wynik demo


Drakes
źródło
8
2 drobne uwagi. (1) Czy nie byłoby lepiej pociąć plik na pierwsze 4 bajty przed odczytem? fileReader.readAsArrayBuffer(blob.slice(0,4))? (2) Czy aby skopiować / wkleić podpisy plików, czy nagłówek nie powinien zawierać początkowych zer for(var i = 0; i < bytes.length; i++) { var byte = bytes[i]; fileSignature += (byte < 10 ? "0" : "") + byte.toString(16); }?
Matthew Madson
1
@Deadpool Zobacz tutaj . Istnieje więcej, mniej popularnych formatów JPEG od różnych producentów. Na przykład FF D8 FF E2= CANNON EOS JPEG FILE, FF D8 FF E3= SAMSUNG D500 JPEG FILE. Kluczowa część podpisu JPEG ma tylko 2 bajty, ale aby zredukować fałszywe alarmy, dodałem najpopularniejsze 4-bajtowe podpisy. Mam nadzieję że to pomogło.
Drakes
23
Jakość tej odpowiedzi jest po prostu niesamowita.
Luca
2
Nie musisz ładować całego obiektu BLOB jako ArrayBuffer, aby określić typ mimeType. Możesz po prostu wyciąć i przekazać pierwsze 4 bajty obiektu blob w następujący sposób:fileReader.readAsArrayBuffer(blob.slice(0, 4))
codeVerine
2
Jaki powinien być czek, aby zezwolić tylko na zwykły tekst? Pierwsze 4 bajty dla plików tekstowych wydają się pierwszymi 4 znakami w pliku tekstowym.
MP Droid
19

Jak stwierdzono w innych odpowiedziach, typ MIME można sprawdzić, sprawdzając podpis pliku w pierwszych bajtach pliku.

Ale inne odpowiedzi to ładowanie całego pliku do pamięci w celu sprawdzenia podpisu, co jest bardzo marnotrawne i może łatwo zawiesić przeglądarkę, jeśli przez przypadek wybierzesz duży plik lub nie.

/**
 * 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
źródło
Myślę, że readyStatezawsze będzie FileReader.DONEw programie obsługi zdarzeń ( specyfikacja W3C ), nawet jeśli wystąpił błąd - czy nie powinno być sprawdzania, czy (!e.target.error)zamiast tego?
boycy
5

Dla każdego, kto nie chce wdrożyć tego samodzielnie, Sindresorhus stworzył narzędzie, które działa w przeglądarce i ma odwzorowania nagłówka na mime dla większości dokumentów, które chcesz.

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

Możesz połączyć sugestię Vitim.us, aby czytać tylko pierwsze X bajtów, aby uniknąć ładowania wszystkiego do pamięci za pomocą tego narzędzia (przykład w 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
źródło
U mnie najnowsza wersja biblioteki nie działała, ale "file-type": "12.4.0"działała i musiałem korzystaćimport * as fileType from "file-type";
ssz
4

Jeśli chcesz tylko sprawdzić, czy przesłany plik jest obrazem, możesz po prostu spróbować załadować go do <img>tagu i sprawdzić, czy nie ma żadnego wywołania zwrotnego.

Przykład:

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
źródło
1
Działa świetnie, próbowałem włamać się do uploadera plików .gif i wyrzucił błąd :)
pathfinder
4

Oto, co musisz zrobić

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

Jeśli chcesz sprawdzić typy plików obrazów, to

if(fileVariable.type.match('image.*'))
{
 alert('its an image');
}
Kailas
źródło
Obecnie nie działa dla: Firefox na Androida, Opera na Androida i Safari na iOS. developer.mozilla.org/en-US/docs/Web/API/File/type
Reid,
3

Oto implementacja Typescript obsługująca webp. Jest to oparte na odpowiedzi JavaScript firmy 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
źródło
1

Jak twierdzi Drake, można to zrobić za pomocą FileReader. Jednak to, co tu przedstawiam, to wersja funkcjonalna. Weź pod uwagę, że dużym problemem przy robieniu tego w JavaScript jest zresetowanie pliku wejściowego. Cóż, ogranicza się to tylko do JPG (w przypadku innych formatów będziesz musiał zmienić typ MIME i magiczną liczbę ):

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

Weź pod uwagę, że zostało to przetestowane w najnowszych wersjach przeglądarek Firefox i Chrome oraz w IExplore 10.

Pełną listę typów MIME można znaleźć w Wikipedii .

Pełną listę magicznych liczb można znaleźć w Wikipedii .

lmiguelmh
źródło
Powyższe linki do Wikipedii są już nieaktualne.
Bob Quinn
@BobQuinn naprawiono, thansk
lmiguelmh
0

Oto rozszerzenie odpowiedzi Roberto14, które wykonuje następujące czynności:

POZWOLI TYLKO NA OBRAZY

Sprawdza, czy FileReader jest dostępny i wraca do sprawdzania rozszerzenia, jeśli nie jest dostępne.

Daje alert o błędzie, jeśli nie jest obrazem

Jeśli jest to obraz, wczytuje podgląd

** Nadal powinieneś przeprowadzić walidację po stronie serwera, jest to bardziej wygoda dla użytkownika końcowego niż cokolwiek innego. Ale jest poręczny!

<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>
pionier
źródło
-1

Krótka odpowiedź brzmi: nie.

Jak zauważyłeś, przeglądarki wywodzą się typez rozszerzenia pliku. Wydaje się, że podgląd na Maca również działa bez rozszerzenia. Zakładam, że to dlatego, że szybciej odczytuje nazwę pliku zawartą we wskaźniku, zamiast patrzeć w górę i czytać plik na dysku.

Zrobiłem kopię pliku jpg o zmienionej nazwie na png.

Udało mi się konsekwentnie uzyskać następujące informacje z obu obrazów w chrome (powinno działać w nowoczesnych przeglądarkach).

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

Które możesz zhakować String.indexOf ('jpeg') sprawdzanie typu obrazu.

Oto skrzypce do odkrycia http://jsfiddle.net/bamboo/jkZ2v/1/

Niejednoznaczna linia, o której zapomniałem skomentować w przykładzie

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

  • Dzieli dane img zakodowane w formacie base64, pozostawiając na obrazie
  • Base64 dekoduje obraz
  • Dopasowuje tylko pierwszą linię danych obrazu

Kod skrzypcowy wykorzystuje dekodowanie base64, które nie działa w IE9, znalazłem fajny przykład użycia skryptu VB, który działa w IE http://blog.nihilogic.dk/2008/08/imageinfo-reading-image-metadata-with.html

Kod do załadowania obrazu pochodzi od Joela Vardy'ego, który przed załadowaniem robi fajne płótno do zmiany rozmiaru obrazu, co może być interesujące https://joelvardy.com/writing/javascript-image-upload

Lex
źródło
1
Nie przeszukuj plików JPEG pod kątem podciągu „jpeg”, to tylko zbieg okoliczności, że znalazłeś go w komentarzu. Pliki JPEG nie muszą go zawierać (a jeśli myślisz o wyszukiwaniu JFIFzamiast tego, cóż APP0, nie musi zawierać JFIF w plikach EXIF-JPEG, więc to też się skończyło).
Kornel,
Zobacz górę „Krótka odpowiedź brzmi: nie”.
Lex,