Countering MIME sniffing with X-Content-Type-Options and Content-Type headers

JavaScript

We’ve learned how to block certain cross-site-scripting (XSS) attacks in the previous article. We did that by disallowing our website from running JavaScript from unknown origins. Unfortunately, attackers might be able to upload malicious code into our application disguised as an image, for example. Then, they can take advantage of MIME sniffing to trick a browser into running it.

This article explains what MIME is and why it is crucial to set our Content-Type header correctly. We also dive into MIME sniffing and add a layer of security using the X-Content-Type-Options header.

MIME

Multipurpose Internet Mail Extensions (MIME) is a standard derived from how emails work, hence the name. The body of an email can contain attachments such as videos and images. The type of each attachment is described using MIME to interpret it.

A valid MIME type consists of a type and a subtype separated by a slash.

The type describes the data type category, such as the .

The subtype specifies the exact sort of data the MIME type describes. For images, it can be , or , for example.

The MIME type can also have optional parameters. A good example is a value that describes the character set used.

An example of a special MIME type is . If you want to know more about it, check out Node.js TypeScript #6. Sending HTTP requests, understanding multipart/form-data

Content-Type

The MIME types are not only used in emails, though. When a browser requests a resource from a given URL, it does not rely on the file extension when processing it. Instead, the browser expects the server to respond with a Content-Type header. Therefore, it should contain a valid MIME type.

Setting valid MIME types

In API with NestJS #55. Uploading files to the server, we’ve defined an API that we can use to upload and retrieve files. Let’s say an attacker uploads the following file through our API:

avatar.jpg

The above code aims to send the cookies from the website. If an attacker retrieves he tcookies containing sensitive data, such as authentication tokens, the consequences can be immense. Because of that, we should not store tokens in regular cookies. If you want to know how to design a safe authentication mechanism using HttpOnly cookies, check out API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies

Also, the same origin policy mechanism could stop the above request from happening. If you want to know more, take a look at Cross-Origin Resource Sharing. Avoiding Access-Control-Allow-Origin CORS error

Since the file is called , our API accepts it without complaining. Because the extension is , JavaScript assumes the MIME type . The crucial part happens when the user tries to access the uploaded file.

The file can be available at where 5 is an example of an id our application can assign to the uploaded file. Therefore, let’s look at a part of the code that retrieves the resource.

Setting informs the browser that it can display the file. If the user tries to save it, the browser suggests as the filename.

The crucial part above is . Thanks to doing that, we respond with a valid .

MIME sniffing

Let’s assume the URL of our application is . Imagine the attacker manages to inject the following HTML into our website successfully:

If we’d set up our Content-Security-Policy header properly, the browser would not execute the above script.

If you want to know more about the content security policy, check out Fighting cross-site-scripting (XSS) with content security policy

In the above case, we would see the following error in the console:

Refused to load the script ‘https://malicious-website.com/malicious-script.js’ because it violates the following Content Security Policy directive: “script-src ‘self’”. Note that ‘script-src-elem’ was not explicitly set, so ‘script-src’ is used as a fallback.

To deal with the above restriction, the attacker might also try injecting the file we’ve seen above that contains the malicious code.

This time, the URL of the script matches the origin of our website. The crucial thing is that the browser does not care that the script’s filename is . On the contrary, it cares about the MIME type. Injecting the above script would result in a different error than before:

Refused to execute script from ‘https://website.com/api/local-files/5’ because its MIME type (‘image/jpeg’) is not executable.

Thankfully, the browser did not execute the malicious script. This is all thanks to the fact that we set up the Content-Type header correctly. Our framework assigns it with the MIME type if we don’t do that. The above type describes generic binary data that we don’t know much about.

When the Content-Type header of a resource is missing or very generic, such as , or , the browser performs MIME sniffing by default.

During the above process, the browser guesses the correct MIME type by looking at the bytes of the resource. Because of that, the browser would interpret as JavaScript and execute it.

X-Content-Type-Options

Guessing the MIME type by the file’s content can pose a significant threat to our users if the attackers know how to take advantage of it. Fortunately, we can deal with the above issue using the header. Furthermore, we can easily add it through middleware if we use Node.js with Express or NestJS.

When the browser fetches , it receives in response. Thanks to that, it does not perform MIME sniffing and throws an error instead.

Refused to execute script from ‘https://website.com/api/local-files/5’ because its MIME type (‘application/octet-stream’) is not executable, and strict MIME type checking is enabled.

We can also use the helmet library instead of setting the X-Content-Type-Options header manually. This is because it sets more headers than X-Content-Type-Options and aims to secure our Express-based applications. If you want to know more, check out the following: Increasing security of Express applications with the Helmet middleware.

Summary

In this article, we’ve gone through what MIME type is and why we should make sure that we send correct types through the Content-Type header. If the browser does not have enough information about the file type, it attempts to guess it. The above can lead to running malicious code on our website and have significant consequences. Therefore, we should use the X-Content-Type-Options header to ensure that the browsers do not try to sniff the MIME type. Doing that makes our application more resilient to attacks.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments