A while ago, I happened to play a lot of topics related to content type, so I decided to wrote an article about it.
As usual, it’s not interesting to talk about the answer directly, so let’s start with three questions:
Question one
In the code below, what is the content type for a.js
to successfully load the code? (Assume MIME type sniffing is off)
For example, text/javascript
is one answer, what else?
1 | <script src="https://example.com/a.js"> |
Question two
What values can be filled in the “???”? For example, text/javascriptis
and module
are both correct answer, what else?
1 | <script type="???"> |
Question three
Now that you have a web page. In order to let browser run script after loaded, what should be the content type in the response?
For example, text/html
and text/xml
are both correct, what else?
Let’s take a look at the answer below.
Question 1: Acceptable content type for <script>
I start thinking about this question because of an XSS challenge made by @ankursundara last year: https://twitter.com/ankursundara/status/1460810934713081862
Part of the code is as follows:
1 | @app.post('/upload') |
Simply put, you can upload any file, but if the file’s MIME type has script
, it will be application/octet-stream
.
X-Content-Type-Optionsit
is set to nosniff
, so we can’t abuse MIME type sniffing.
The goal is to successfully execute XSS.
It is not difficult to see from the above code that an HTML file can be uploaded, but because of script-src 'self'
CSP, even if HTML can be uploaded, inline script cannot be used.
We can only import script this way: <script src="/uploads/xxx">
.
But, if the content type of /uploads/xxx
is application/octet-stream
, Chrome will throw following error:
Refused to execute script from ‘https://uploader.c.hc.lc/uploads/xxx' because its MIME type (‘application/octet-stream’) is not executable, and strict MIME type checking is enabled.
So the goal of this question is very clear, to find a MIME type that does not contain script
but can be successfully loaded by the browser.
After seeing this challenge, my first idea is to check the source code of Chromium, it’s easier to find the related part by googling the error message:"strict MIME type checking is enabled" site:https://chromium.googlesource.com/
We can find this related file through the search results: https://chromium.googlesource.com/chromium/blink/+/refs/heads/main/Source/core/dom/ScriptLoader.cpp
This file is very old and deprecated, but at least we know it’s part of blink, so we can find a similiar file in the latest codebase, what I found is: third_party/blink/renderer/core/script/script_loader.cc
You can find this function: IsValidClassicScriptTypeAndLanguage
1 |
|
Then, we can search this keyword:IsSupportedJavaScriptMIMEType
and find this file: third_party/blink/common/mime_util/mime_util.cc
1 |
|
You can also see the URL of the spec from the comments. The list given is the same, and this list is basically the answer to the first question. The above MIME types can be loaded as script.
But we can find one thing, that is, every MIME type contains script
.
At that time, I got stuck at this point. Later, the author released a hint: Origin Trials
. Follow the hint I found a feature called Web Bundles. This is the answer to the XSS challenge.
What is Web Bundles?
To put it simply, Web Bundles is a feature that you can package a bunch of data (HTML, CSS, JS…) together into a .wbn file. The above article mentions an example that your friend wants to share with a web game with you, but he can’t do it because there is no internet connection.
But through the Web Bundles, he can package the web game into a .wbn file and send it to you. After you receive it via bluetooth or airdrop, you can just open it in the browser, just like an app.
In addition to loading the entire app, you can also load specific resources from the Web Bundle. You can find the detail here:Explainer: Subresource loading with Web Bundles.
Here is the example from the article:
1 | <script type="webbundle"> |
When you load https://example.com/dir/a.js
, the browser will first go to subresources.wbn to find this resource, instead of reaching to the server to download it directly.
So, for the XSS challenge I mentioned in the beginning, the answer is to bundle the JavaScript into a web bundle file, and then load it. It’s MIME type is application/webbundle
, so it’s allow.
After web bundle is loaded, we can load script from it.
But why didn’t we see this feature when we looked at the Chromium code?
This is because we are too focus on MIME type, so we only look atIsValidClassicScriptTypeAndLanguage
, but we should see another function who call it: GetScriptTypeAtPrepare:
1 | ScriptLoader::ScriptTypeAtPrepare ScriptLoader::GetScriptTypeAtPrepare( |
Calling IsValidClassicScriptTypeAndLanguage
is just the first step, there are other type
as well, and it’s the answer to question two.
Question 2: Acceptable types of <script>
Like previous question, it’s also about a CTF challenge. There is a challenge called YACA in PlaidCTF 2022, here is the offical writeup: https://github.com/zwade/yaca/tree/master/solution
We know from the code I just posted that the answer to this question is the answer to the first question (that pile of MIME types) plus the following four types:
- module
- importmap
- speculationrules
- webbundle
We already know module
and webbundle
, so let’s take a look at importmap and specificationrules.
The specification of import map is here: https://github.com/WICG/import-maps
What is the problem import map wants to solve?
Although the browser already supports module and import, you still can’t do this on the browser:
1 | import moment from "moment"; |
You can only write like this:
1 | import moment from "/node_modules/moment/src/moment.js"; |
import map want to solve this problem by introducing a mapping table:
1 | <script type="importmap"> |
The challenge we mentioned can be solve by changing the file like this:
1 |
|
Let’s take a look at speculationrules, here is the spec: https://github.com/WICG/nav-speculation
This feature is mainly to solve some problems caused by pre-rendering, I haven’t delved into it. It works like this:
1 | <script type="speculationrules"> |
It uses a JSON file for the pre-render rule, quite different from <link rel="prerender">
.
Question 3: content type
It’s from a challenge called PlanetSheet in Securinets CTF Quals 2022. When the content type is text/xsl
, we can run script via <x:script>
.
This classic research is mentioned in each writeup: Content-Type Research , you can find the detail in it.
The following five content types can execute XSS in all browsers:
- text/html
- application/xhtml+xml
- application/xml
- text/xml
- image/svg+xml
I was curious about this behavior, so I checked the source code of Chromium a bit, and found other two content types that are always put together with the others:
- application/rss+xml
- application/atom+xml
Code: xsl_style_sheet_resource.cc
1 | static void ApplyXSLRequestProperties(FetchParameters& params) { |
However, these two will not be loaded as XML, so I searched and found this bug: Issue 104358: Consider allowing more types to parse as XML, which mentioned a commit in 2009:
1 | if (mime_type == "application/rss+xml" || |
Because the RSS feed may contain third-party cotnent, it’s vulnerable to XSS if it is rendered as XML, so these two are forcibly turned off.
By the way, there is a awesome tool for searching source code: https://sourcegraph.com/search