212

I have read this and this questions which seems to suggest that the file MIME type could be checked using javascript on client side. Now, I understand that the real validation still has to be done on server side. I want to perform a client side checking to avoid unnecessary wastage of server resource.

To test whether this can be done on client side, I changed the extension of a JPEG test file to .png and choose the file for upload. Before sending the file, I query the file object using a javascript console:

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

This is what I get on Chrome 28.0:

File {webkitRelativePath: "", lastModifiedDate: Tue Oct 16 2012 10:00:00 GMT+0000 (UTC), name: "test.png", type: "image/png", size: 500055…}

It shows type to be image/png which seems to indicate that the checking is done based on file extension instead of MIME type. I tried Firefox 22.0 and it gives me the same result. But according to the W3C spec, MIME Sniffing should be implemented.

Am I right to say that there is no way to check the MIME type with javascript at the moment? Or am I missing something?

Community
  • 1
  • 1
Question Overflow
  • 9,707
  • 18
  • 68
  • 108
  • 5
    `I want to perform a client side checking to avoid unnecessary wastage of server resource.` I don't understand how why you say that validation has to be done on the server side, but then say you want to reduce server resources. Golden rule: **Never trust user input**. What's the point of checking the MIME type on the client side if you're then just doing it on the server side. Surely that's an "unnecessary wastage of *client* resource"? – Ian Clark Aug 18 '13 at 14:09
  • 8
    Providing better file type checking/feedback to users client-side is a good idea. However, as you have stated, browsers simply rely on the file extensions when determining the value of the `type` property for `File` objects. The webkit source code, for example, reveals this truth. It is possible to accurately identify files client-side by looking for "magic bytes" in the files, among other things. I'm currently working on an MIT library (in what little free time I have) that will do just that. If you're interested in my progress, have a look at https://github.com/rnicholus/determinater. – Ray Nicholus Aug 18 '13 at 14:21
  • 39
    @IanClark, the point is that if the file is of an invalid type, I can reject it on client side rather than waste the upload bandwidth only to reject it on the server side. – Question Overflow Aug 18 '13 at 14:55
  • @RayNicholus, cool dude! Will look through it when I have the time. Thanks :) – Question Overflow Aug 18 '13 at 14:57
  • Are you sure that your test file still has the mimetype `image/jpeg`, and you didn't actually modify that by changing the extension? – Bergi Jun 04 '14 at 03:31
  • Mime type is not a magic bullet, it is just an assumption. Binary files themselves does not carry such a property, so there is no way to seamlessly "get it" client side. It is set BY SERVERS when sending data TO CLIENTS, but even then it is often guessed by file extension, or.. explicitly set by backend developers who know what content type they are sending out. http://en.wikipedia.org/wiki/Mime_type – Ingmars Jun 25 '14 at 16:23
  • 1
    @QuestionOverflow A little late, but I've added a complete solution and a live, working demo in my answer. Enjoy. – Drakes May 08 '15 at 09:00

9 Answers9

412

You can easily determine the file MIME type with JavaScript's FileReader before uploading it to a server. I agree that we should prefer server-side checking over client-side, but client-side checking is still possible. I'll show you how and provide a working demo at the bottom.


Check that your browser supports both File and Blob. All major ones should.

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

Step 1:

You can retrieve the File information from an <input> element like this (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>

Here is a drag-and-drop version of the above (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>

Step 2:

We can now inspect the files and tease out headers and MIME types.

✘ Quick method

You can naïvely ask Blob for the MIME type of whatever file it represents using this pattern:

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

For images, MIME types come back like the following:

image/jpeg
image/png
...

Caveat: The MIME type is detected from the file extension and can be fooled or spoofed. One can rename a .jpg to a .png and the MIME type will be be reported as image/png.


✓ Proper header-inspecting method

To get the bonafide MIME type of a client-side file we can go a step further and inspect the first few bytes of the given file to compare against so-called magic numbers. Be warned that it's not entirely straightforward because, for instance, JPEG has a few "magic numbers". This is because the format has evolved since 1991. You might get away with checking only the first two bytes, but I prefer checking at least 4 bytes to reduce false positives.

Example file signatures of JPEG (first 4 bytes):

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

Here is the essential code to retrieve the file header:

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

You can then determine the real MIME type like so (more file signatures here and here):

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

Accept or reject file uploads as you like based on the MIME types expected.


Demo

Here is a working demo for local files and remote files (I had to bypass CORS just for this demo). Open the snippet, run it, and you should see three remote images of different types displayed. At the top you can select a local image or data file, and the file signature and/or MIME type will be displayed.

Notice that even if an image is renamed, its true MIME type can be determined. See below.

Screenshot

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/>
Hassan Baig
  • 12,135
  • 18
  • 66
  • 155
Drakes
  • 20,841
  • 3
  • 44
  • 84
  • 8
    2 minor comments. (1) Wouldn't it be better to slice the file to it's first 4 bytes prior to reading? `fileReader.readAsArrayBuffer(blob.slice(0,4))`? (2) In order to copy/paste file signatures, shouldn't the header be constructed with leading 0's `for(var i = 0; i < bytes.length; i++) { var byte = bytes[i]; fileSignature += (byte < 10 ? "0" : "") + byte.toString(16); }` ? – Matthew Madson Sep 02 '15 at 19:52
  • @drakes where did you get file signature `ffd8ffe2`? Based on the wiki link you gave the file signatures for `jpeg` are `ffd8ffdb`, `ffd8ffe0`, and `ffd8ffe1`. Can you confirm? Thanks – Jo E. Nov 02 '15 at 09:15
  • 1
    @Deadpool See [here](http://filesignatures.net/index.php?page=all&order=SIGNATURE&sort=DESC&alpha=). There are more, less common, JPEG formats from different makers. For example, `FF D8 FF E2` = CANNON EOS JPEG FILE, `FF D8 FF E3` = SAMSUNG D500 JPEG FILE. The key part of the JPEG signature is only 2 bytes, but to reduce false positives I added the most common 4-byte signatures. I hope that helps. – Drakes Nov 04 '15 at 03:35
  • It looks like `FF D8 FF` is consistent in JPEG files. I think its safe to assume that only JPEG files have the pattern. It would be difficult to keep track of the `4-byte` signature especially when some of the signatures are manufacturer dependent. Thanks @Drakes ! – Jo E. Nov 04 '15 at 04:24
  • 42
    The quality of this answer is just amazing. – Luca Nov 04 '15 at 15:04
  • Proper header-inspecting method is nice!! – Fábio Zangirolami Aug 30 '16 at 14:33
  • Has anyone tried to port libmagic over into javascript library available for the browser? – Ben Creasy May 16 '17 at 02:38
  • 1
    Hi, this is a great answer. It is however not clear to me how to get the header information to decide whether to accept the file for upload or not. I want to make this decision inside the onchange event of the input field. Could you please clarify how I can achieve this? Sorry, Javascript beginner here. – mod0 Aug 25 '17 at 01:14
  • Typescript says: property `result` does not exist on type `EventTarget`... What gives? – Pandem1c Oct 25 '17 at 16:57
  • 2
    You don't have to load complete blob as ArrayBuffer to determine the mimeType. You can just slice and pass first 4 bytes of the blob like this: ```fileReader.readAsArrayBuffer(blob.slice(0, 4))``` – codeVerine Apr 12 '18 at 13:41
  • 3
    What should be the check to allow only plain text? The first 4 bytes for text files seem to the first 4 characters in the text file. – MP Droid Oct 07 '18 at 20:25
  • I just encountered a JPEG with ffd8ffdb as signature. Not sure what its origin is. – asiop Jul 06 '20 at 09:53
  • Read just first chunk? blob.stream().getReader().read().then(({ value }) => { new Uint32Array(value.buffer)[0] // header }); – William Lohan Jul 10 '20 at 07:30
  • This won't work if you need to check signatures which contains values lower than or equal to `0x09` because the conversion from hex to string does take care of padding. – tigrou Feb 23 '21 at 16:36
21

As stated in other answers, you can check the mime type by checking the signature of the file in the first bytes of the file.

But what other answers are doing is loading the entire file in memory in order to check the signature, which is very wasteful and could easily freeze your browser if you select a big file by accident or not.

/**
 * 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
  • 16,145
  • 12
  • 81
  • 96
  • I think `readyState` will always be `FileReader.DONE` in the event handler ([W3C spec](https://www.w3.org/TR/FileAPI/#dfn-readyState)) even if there was an error - shouldn't the check be if `(!e.target.error)` instead? – boycy Jun 29 '18 at 15:20
9

For anyone who's looking to not implement this themselves, Sindresorhus has create a utility that works in the browser and has the header-to-mime mappings for most documents you could want.

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

You could combine Vitim.us's suggestion of only reading in the first X bytes to avoid loading everything into memory with using this utility (example 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);
Eugene
  • 702
  • 7
  • 21
Vinay
  • 5,510
  • 5
  • 34
  • 52
  • 1
    For me, the latest version of the library didn't work but the `"file-type": "12.4.0"` worked and I had to use `import * as fileType from "file-type";` – ssz Apr 26 '20 at 06:35
5

This is what you have to do

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

If you want to check for image file types then

if(fileVariable.type.match('image.*'))
{
 alert('its an image');
}
Kailas
  • 403
  • 1
  • 5
  • 14
  • Currently not working for: Firefox for Android, Opera for Android, and Safari on iOS. https://developer.mozilla.org/en-US/docs/Web/API/File/type – Reid Jul 10 '19 at 15:20
4

If you just want to check if the file uploaded is an image you can just try to load it into <img> tag an check for any error callback.

Example:

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
  • 663
  • 4
  • 19
4

Here is a Typescript implementation that supports webp. This is based on the JavaScript answer by 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
  • 342
  • 3
  • 19
2

As Drake states this could be done with FileReader. However, what I present here is a functional version. Take in consideration that the big problem with doing this with JavaScript is to reset the input file. Well, this restricts to only JPG (for other formats you will have to change the mime type and the magic number):

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

Take in consideration that this was tested on latest versions of Firefox and Chrome, and on IExplore 10.

For a complete list of mime types see Wikipedia.

For a complete list of magic number see Wikipedia.

lmiguelmh
  • 2,259
  • 27
  • 46
0

Here is an extension of Roberto14's answer that does the following:

THIS WILL ONLY ALLOW IMAGES

Checks if FileReader is available and falls back to extension checking if it is not available.

Gives an error alert if not an image

If it is an image it loads a preview

** You should still do server side validation, this is more a convenience for the end user than anything else. But it is handy!

<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>
pathfinder
  • 1,471
  • 16
  • 21
-1

Short answer is no.

As you note the browsers derive type from the file extension. Mac preview also seems to run off the extension. I'm assuming its because its faster reading the file name contained in the pointer, rather than looking up and reading the file on disk.

I made a copy of a jpg renamed with png.

I was able to consistently get the following from both images in chrome (should work in modern browsers).

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

Which you could hack out a String.indexOf('jpeg') check for image type.

Here is a fiddle to explore http://jsfiddle.net/bamboo/jkZ2v/1/

The ambigious line I forgot to comment in the example

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

  • Splits the base64 encoded img data, leaving on the image
  • Base64 decodes the image
  • Matches only the first line of the image data

The fiddle code uses base64 decode which wont work in IE9, I did find a nice example using VB script that works in IE http://blog.nihilogic.dk/2008/08/imageinfo-reading-image-metadata-with.html

The code to load the image was taken from Joel Vardy, who is doing some cool image canvas resizing client side before uploading which may be of interest https://joelvardy.com/writing/javascript-image-upload

Lex
  • 3,642
  • 2
  • 34
  • 56
  • 1
    Please don't search JPEGs for the "jpeg" substring, that's just a coincidence you found it in a comment. JPEG files don't have to contain it (and if you're thinking about searching for `JFIF` instead, well `APP0` doesn't have to contain JFIF in EXIF-JPEGs so that's out too). – Kornel Jul 07 '14 at 02:00
  • See top "Short answer is no". – Lex Jul 07 '14 at 04:48