Эксплуатация MIME Sniffing
В каждом запросе обычно есть заголовок ответа, называемый Content-Type
, который сообщает браузеру MIME-тип
ответа, такой как text/html
или application/json
.
Но что, если заголовка Content-Type
нет? Браузер попытается определить тип на основе содержимого файла. Даже если заголовок Content-Type
присутствует, браузер все равно может интерпретировать его как другой тип.
Это поведение, при котором MIME-тип
выводится из содержимого файла, называется MIME sniffing
. Давайте вместе исследуем эту функцию!
MIME Sniffing 101
Мы можем легко отправить ответ без заголовка Content-Type
, используя Express:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.write('<h1>hello</h1>')
res.end()
});
app.listen(5555, () => {
console.log('Server is running on port 5555');
});
Если вы откроете эту веб-страницу в браузере, вы увидите, что текст "hello" становится больше и жирным, что указывает на то, что браузер отображает ответ как веб-страницу:

Теперь давайте посмотрим на второй пример, где мы изменяем <h1>
на <h2>
:
const express = require('express');const app = express();app.get('/', (req, res) => { res.write('<h2>hello</h2>') res.end()});app.listen(5555, () => { console.log('Server is ruconst express = require('express');
const app = express();
app.get('/', (req, res) => {
res.write('<h2>hello</h2>');
res.end();
});
app.listen(5555, () => {
console.log('Server is running on port 5555');
});
nning on port 5555');});

Почему это вдруг отображается как простой текст?
Теперь в третьем примере у нас есть тот же <h1>
, но с дополнительным текстом:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.write('Hello, <h1>world</h1>');
res.end();
});
app.listen(5555, () => {
console.log('Server is running on port 5555');
});

Снова он отображается как простой текст вместо HTML.
Вы можете подумать, что механизм MIME sniffing
в браузере — это загадка, как черный ящик, о том, как он работает. К счастью, мы тестируем в Chrome, и Chromium имеет открытый исходный код.
Код, отвечающий за MIME sniffing
в Chromium, можно найти в файле net/base/mime_sniffer.cc. В начале кода объясняется, как это работает:
// Detecting mime types is a tricky business because we need to balance
// compatibility concerns with security issues. Here is a survey of how other
// browsers behave and then a description of how we intend to behave.
//
// HTML payload, no Content-Type header:
// * IE 7: Render as HTML
// * Firefox 2: Render as HTML
// * Safari 3: Render as HTML
// * Opera 9: Render as HTML
//
// Here the choice seems clear:
// => Chrome: Render as HTML
//
// HTML payload, Content-Type: "text/plain":
// * IE 7: Render as HTML
// * Firefox 2: Render as text
// * Safari 3: Render as text (Note: Safari will Render as HTML if the URL
// has an HTML extension)
// * Opera 9: Render as text
//
// Here we choose to follow the majority (and break some compatibility with IE).
// Many folks dislike IE's behavior here.
// => Chrome: Render as text
//
// We generalize this as follows. If the Content-Type header is text/plain
// we won't detect dangerous mime types (those that can execute script).
//
// HTML payload, Content-Type: "application/octet-stream":
// * IE 7: Render as HTML
// * Firefox 2: Download as application/octet-stream
// * Safari 3: Render as HTML
// * Opera 9: Render as HTML
//
// We follow Firefox.
// => Chrome: Download as application/octet-stream
// One factor in this decision is that IIS 4 and 5 will send
// application/octet-stream for .xhtml files (because they don't recognize
// the extension). We did some experiments and it looks like this doesn't occur
// very often on the web. We choose the more secure option.
Итак, что заставляет его считать полезную нагрузку "HTML payload"? Это проверяется дальше в коде:
// Our HTML sniffer differs slightly from Mozilla. For example, Mozilla will
// decide that a document that begins "<!DOCTYPE SOAP-ENV:Envelope PUBLIC " is
// HTML, but we will not.
#define MAGIC_HTML_TAG(tag) \
MAGIC_STRING("text/html", "<" tag)
static const MagicNumber kSniffableTags[] = {
// XML processing directive. Although this is not an HTML mime type, we sniff
// for this in the HTML phase because text/xml is just as powerful as HTML and
// we want to leverage our white space skipping technology.
MAGIC_NUMBER("text/xml", "<?xml"),
// Mozilla
// DOCTYPEs
MAGIC_HTML_TAG("!DOCTYPE html"),
// HTML5 spec
// Sniffable tags, ordered by how often they occur in sniffable documents.
MAGIC_HTML_TAG("script"),
// HTML5 spec, Mozilla
MAGIC_HTML_TAG("html"),
// HTML5 spec, Mozilla
MAGIC_HTML_TAG("!--"),
MAGIC_HTML_TAG("head"),
// HTML5 spec, Mozilla
MAGIC_HTML_TAG("iframe"),
// Mozilla
MAGIC_HTML_TAG("h1"),
// Mozilla
MAGIC_HTML_TAG("div"),
// Mozilla
MAGIC_HTML_TAG("font"),
// Mozilla
MAGIC_HTML_TAG("table"),
// Mozilla
MAGIC_HTML_TAG("a"),
// Mozilla
MAGIC_HTML_TAG("style"),
// Mozilla
MAGIC_HTML_TAG("title"),
// Mozilla
MAGIC_HTML_TAG("b"),
// Mozilla
MAGIC_HTML_TAG("body"),
// Mozilla
MAGIC_HTML_TAG("br"),
MAGIC_HTML_TAG("p"),
// Mozilla
};
// ...
// Returns true and sets result if the content appears to be HTML.
// Clears have_enough_content if more data could possibly change the result.
static bool SniffForHTML(base::StringPiece content,
bool*
Он проверяет, соответствует ли строка в начале ответа, после удаления пробелов, перечисленным выше шаблонам HTML. Общие начала HTML, такие как <!DOCTYPE html
и <html
, включены в этот список. Это также объясняет, почему только пример <h1>hello</h1>
отображается как HTML в наших предыдущих тестах.
Согласно исходному коду, похоже, что Chromium не учитывает расширение файла или другие факторы в URL. Он полностью полагается на содержимое файла. Давайте проведем еще один тест, чтобы это подтвердить:
const express = require('express');
const app = express();
app.get('/test.html', (req, res) => {
res.write('abcde<h1>test</h1>');
res.end();
});
app.listen(5555, () => {
console.log('Server is running on port 5555');
});
Я не буду включать изображение здесь, но независимо от того, что URL — это test.html, результат все равно отображается как простой текст. А как насчет других браузеров? Давайте попробуем открыть его в Firefox:

Мы можем увидеть, что Firefox действительно отображает его как HTML! Следовательно, мы можем сделать вывод, что Firefox учитывает расширение файла в URL при выполнении MIME sniffing
.
Exploiting MIME Sniffing for Attacks
Из нашего предыдущего исследования мы подтвердили факт: если у ответа нет установленного Content-Type
и мы можем контролировать содержимое, мы можем использовать MIME sniffing
, чтобы заставить браузер интерпретировать файл как веб-страницу.
Например, предположим, что есть функция загрузки изображений, которая проверяет только расширение файла, но не содержимое. Мы можем загрузить файл с именем a.png, но содержимое будет <script>alert(1)</script>
. Если сервер не добавляет автоматически Content-Type
при обслуживании этого изображения, это становится уязвимостью XSS.
Однако большинство серверов в настоящее время автоматически добавляют Content-Type
. Возможно ли это все еще?
Да, мы можем объединить это с другими незначительными проблемами.
Apache HTTP Server — это широко используемый сервер, и знаменитый стек LAMP (Linux + Apache + MySQL + PHP) использует этот сервер.
Apache HTTP Server имеет своеобразное поведение: если имя файла содержит только точку (.), он не выведет Content-Type
. Например, a.png автоматически определит MIME-тип
на основе расширения файла и выведет image/png
, но если имя файла будет ..png, он не выведет Content-Type
.
Таким образом, если бэкенд использует Apache HTTP Server для обработки загрузок файлов, мы можем загрузить, казалось бы, валидное изображение с расширением ..png, но при открытии в браузере оно будет отображаться как веб-страница, становясь уязвимостью XSS.
Согласно Apache HTTP Server, это ожидаемое поведение, и вы можете обратиться к @YNizry для получения дополнительных деталей.
Content Types that can execute JavaScript
Кроме HTML-файлов, какие еще типы файлов могут выполнять JavaScript?
В исследовании, проведенном BlackFan в 2020 году: Content-Type Research предоставлен исчерпывающий список: Content-Type that can be used for XSS.
Из списка мы видим, что кроме самых распространенных типов контента HTML, XML и SVG есть и другие, которые могут выполнять JavaScript.
Особый интерес представляют файлы SVG, потому что многие веб-сайты имеют функциональность загрузки изображений, и SVG считается форматом изображения. Поэтому некоторые веб-сайты разрешают загрузку SVG. Однако из этого исследования мы можем узнать, что разрешение загрузки SVG эквивалентно разрешению загрузки HTML, поскольку SVG может выполнять JavaScript!
Например, febin сообщил о уязвимости в программном обеспечении с открытым исходным кодом Mantis Bug Tracker в 2022 году: CVE-2022-33910: Stored XSS via SVG file upload. Эта уязвимость возникла потому, что пользователи могли загружать файлы при создании нового репорта, и разрешенный формат файла был SVG. Поэтому файл SVG мог быть загружен, и когда другие пользователи открывали его, скрытый код внутри выполнялся.
Content Types that can be loaded as scripts
Возьмем следующий фрагмент кода в качестве примера:
<script src="URL"></script>
Вы когда-нибудь задумывались, какой должен быть тип контента URL, чтобы браузер загрузил его как скрипт?
Например, если это image/png
, это не сработает, и вы увидите следующее сообщение об ошибке в браузере:
Refused to execute script from 'http://localhost:5555/js' because its MIME type ('image/png') is not executable.
Наиболее распространенный тип, text/javascript
, очевидно, работает нормально. Но есть ли другие?
Из десяти перечисленных ниже типов контента только два из них не сработают. Можете угадать, какие два?
application/zip
application/json
application/octet-stream
text/csv
text/html
text/json
text/plain
huli/blog
video/mp4
font/woff2
Ответ будет раскрыт позже, но сначала давайте рассмотрим "действительные MIME-типы JavaScript", указанные в исходном коде Chromium: /third_party/blink/common/mime_util/mime_util.cc:
// Support every script type mentioned in the spec, as it notes that "User
// agents must recognize all JavaScript MIME types." See
// https://html.spec.whatwg.org/#javascript-mime-type.
const char* const kSupportedJavascriptTypes[] = {
"application/ecmascript",
"application/javascript",
"application/x-ecmascript",
"application/x-javascript",
"text/ecmascript",
"text/javascript",
"text/javascript1.0",
"text/javascript1.1",
"text/javascript1.2",
"text/javascript1.3",
"text/javascript1.4",
"text/javascript1.5",
"text/jscript",
"text/livescript",
"text/x-ecmascript",
"text/x-javascript",
};
Все вышеперечисленные — это действительные MIME-типы JavaScript, и вы можете увидеть множество типов из прошлого, таких как jscript
или livescript
.
Кроме действительных MIME-типов JavaScript, согласно спецификации, есть только четыре типа, которые не сработают:
audio/*
image/*
video/*
text/csv
Кроме этих, все остальные типы действительны. Таким образом, среди упомянутых выше вариантов только text/csv
и video/mp4
не сработают, в то время как остальные будут работать! Да, даже text/html
и application/json
будут работать, и даже xss/blog
будет работать.
Если вы хотите ужесточить этот механизм и разрешить загрузку только MIME-типов JavaScript, вы можете добавить заголовок в ответ: X-Content-Type-Options: nosniff
. После добавления этого заголовка ни один из 10 ранее упомянутых примеров не будет работать, и вы увидите следующее сообщение об ошибке при загрузке:
Refused to execute script from 'http://localhost:5555/js' because its MIME type ('text/plain') is not executable, and strict MIME type checking is enabled.
strict MIME type
это функция, включаемая добавлением этого заголовка.
То же самое касается таблиц стилей. После включения этой функции только MIME-тип text/css
будет признан действительным, и все остальные приведут к ошибке.
Итак, что произойдет, если вы продолжите включать эту функцию? Это создаст дополнительный риск безопасности.
Предположим, вы случайно нашли уязвимость XSS на веб-сайте. Однако проблема в том, что CSP
веб-сайта — это script-src 'self';
, что означает, что он не позволяет загружать внешние скрипты, и встроенные скрипты также заблокированы. Как вы можете обойти CSP
в этом случае?
Если веб-сайт предоставляет функцию загрузки файлов, которая принимает файлы, отличные от изображений, видео и CSV-файлов, и не проверяет содержимое, предположим, что он принимает только ZIP-файлы, тогда вы можете загрузить файл, содержащий код JavaScript.
Таким образом, вы можете использовать <script src="/uploads/files/my.zip"></script>
, чтобы загрузить скрипт и успешно обойти CSP
. Причина, по которой это работает, заключается в поведении, упомянутом ранее — пока MIME-тип не является одним из тех немногих типов, его можно загрузить как скрипт.
Вот почему вы видите, что многие веб-сайты добавляют этот заголовок, чтобы предотвратить такое поведение.
Заключение
В этой статье мы рассмотрели множество интересных аспектов, связанных с MIME-типами, а также изучили много исходного кода Chromium. Лично я считаю, что исходный код Chromium написан очень понятно, включает комментарии и ссылки на спецификации, поэтому вам не нужно отдельно искать спецификации. Это как убить двух зайцев одним выстрелом.
Наконец, мы поняли цель и функцию заголовка X-Content-Type-Options: nosniff
. Я уверен, что многие люди видели этот заголовок раньше, но не знали, для чего он нужен, и теперь вы это знаете.
Дополнительные ссылки:
Last updated