Какие предположения можно сделать относительно следующего HTTP ответа сервера?
![](https://habrastorage.org/getpro/habr/upload_files/cf2/890/aaa/cf2890aaa371fbacea2d4e678f80ff5e.png)
Глядя на этот небольшой фрагмент HTTP ответа, можно предположить, что веб-приложение, вероятно, содержит уязвимость XSS.
Почему это возможно? Что обращает на себя внимание в этом ответе сервера?
Вы будете правы, если сомневаетесь насчет заголовка Content-Type. В нем есть незначительный недостаток - отсутствие атрибута charset. Это может казаться неважным, однако, в этой статье мы объясним, как злоумышленники могут использовать этот недостаток для внедрения произвольного JavaScript кода на веб-сайт, сознательно изменяя набор символов, который ожидает браузер.
Кодировка символов
Типичный заголовок Content-Type в HTTP ответе выглядит следующим образом:
![](https://habrastorage.org/getpro/habr/upload_files/532/d70/71c/532d7071c9a39d14f4755578fa74683d.png)
Атрибут charset сообщает браузеру, что для кодирования тела HTTP ответа использовалась кодировка UTF-8. Кодировка символов, такая как UTF-8, определяет соответствие между символами и байтами. Когда веб-сервер обрабатывает HTML документ, он соотносит символы документа с соответствующими байтами и передает их в теле HTTP ответа. Этот процесс преобразует (кодирует) символы в байты:
![](https://habrastorage.org/getpro/habr/upload_files/67f/ba5/cea/67fba5cea51b2c860ca5c533da0ebe29.png)
Когда браузер получает эти байты в теле HTTP ответа, он может преобразовать их обратно (декодировать) в символы HTML документа:
![](https://habrastorage.org/getpro/habr/upload_files/f3a/036/8a5/f3a0368a58b608f99618e5019457207b.png)
UTF-8 является одним из многих способов кодирования набора символов, которые современные браузеры обязаны поддерживать в соответствии со спецификацией HTML. Также существуют множество других алгоритмов, вроде UTF-16, ISO-8859-xx, windows-125x, GBK, Big5, и т.д. Знание того, какую кодировку использовал сервер, является критически важным для браузера, поскольку без этого он не сможет правильно декодировать байты в теле HTTP ответа.
Но, что произойдет, если атрибут charset не указан в заголовке Content-Type или он указан неверно?
В таком случае браузер будет искать тэг в самом HTML документе. Этот тэг также может содержать атрибут charset, который определяет кодировку символов (<meta charset="UTF-8">). Это правило для браузера: для чтения HTML документа, необходимо сначала декодировать тело HTTP ответа. Таким образом, браузеру необходимо сначала сделать предположение о некотором способе кодирования тела HTTP ответа, потом декодировать его, найти тег и, возможно, повторно декодировать байты с указанной кодировкой символов.
Другим, менее распространенным подходом к указанию кодировки символов является указание порядка байтов Byte-Order Mark (BOM). Это специальный символ Unicode (U+FEFF), который может быть помещен перед строкой для указания порядка байтов и кодировки символов. В основном он используется в файлах, но поскольку эти файлы могут передаваться через веб-сервер, современные браузеры поддерживают этот способ. Метка порядка байтов в начале HTML документа даже имеет приоритет над атрибутом charset в заголовке Content-Type и тэге <meta>.
Подводя итог, существуют три распространенных способа, которые браузер использует для определения кодировки символов HTML-документа, упорядоченных по приоритету:
Метка порядка байтов в начале HTML документа,
атрибут charset в заголовке Content-Type,
тэг <meta> в HTML документе.
Отсутствие информации о кодировке
Метка порядка байтов обычно используется очень редко, а атрибут charset не всегда присутствует в заголовке Content-Type или может быть указан неверно. Кроме того, особенно для частичных HTML-ответов от сервера, обычно отсутствует тег <meta>, указывающий кодировку символов. В этих случаях браузер не располагает никакой информацией о том, какой набор символов следует использовать:
![](https://habrastorage.org/getpro/habr/upload_files/4b3/741/416/4b37414163b3f09d8892e32df19127c9.png)
Вы когда-нибудь видели подобное сообщение об ошибке? Вероятно, нет, потому что его не существует.
Как и в случае с неправильным синтаксисом HTML, браузеры пытаются восстановить недостающую информацию о наборе символов при анализе полученного от веб-сервера контента, и извлечь из этого максимум возможного. Такое нестрогое поведение способствует хорошему взаимодействию с пользователем, но также может открыть возможности для использования таких техник эксплуатации, как mXSS.
При отсутствии информации о кодировке символов браузеры пытаются сделать предположение, основываясь на содержимом, что называется автоматическим определением (auto-detection). Это похоже на поиск по типу MIME (MIME-type sniffing), но работает на уровне кодировки символов. Например, движок рендеринга Chromium Blink использует библиотеку Compact Encoding Detection (CED) для автоматического определения кодировки символов. Как мы увидим дальше, с точки зрения злоумышленника, функция автоматического определения кодировки символов является очень мощным инструментом.
На данный момент мы познакомились с различными механизмами, которые используют браузеры для определения кодировки символов HTML документов. Но как злоумышленники могут это использовать?
Различия в кодировках
Цель кодирования символов - преобразовать символы в последовательность байтов, обрабатываемую компьютером. Эти байты передаются по сети и декодируются обратно в символы получателем. Таким образом, восстанавливаются все те же символы, которые хотел передать отправитель:
![](https://habrastorage.org/getpro/habr/upload_files/38e/8a5/541/38e8a5541784193af5ff8cc5be332e59.png)
Это работает хорошо только в том случае, когда отправитель и получатель согласовали способ кодировки символов. Если есть несоответствие между кодировкой символов, используемой для кодирования и декодирования, то получатель может увидеть разные символы:
![](https://habrastorage.org/getpro/habr/upload_files/91f/fd0/b3d/91ffd0b3d214ce575c3c6246f937519a.png)
Подобное несоответствие между кодировкой символов, используемой для кодирования и декодирования, и есть то, что мы называем различием в кодировках в этой статье.
Для веб-приложений жизненно важно, когда контролируемые пользователем данные очищаются для предотвращения уязвимостей Cross-Site Scripting (XSS). Если кодировка символов, используемая браузером, отличается от той, которую использует веб-сервер, это теоретически может нарушить очистку данных и привести к уязвимостям XSS.
Само по себе это не является большой новостью, и даже Google сталкивался с подобной проблемой еще в 2005 году. Страница Google 404 не предоставляла информации о кодировке символов, что могло быть проэксплуатировано через добавление полезной нагрузки XSS в кодировке UTF-7, В UTF-7 специальные символы HTML, такие как угловые скобки, кодируются иначе, чем в ASCII, что может быть использовано для обхода процедуры санитизации:
![](https://habrastorage.org/getpro/habr/upload_files/f3d/67b/7ea/f3d67b7eaa85cf28c60e61317a12523c.png)
Это наглядно демонстрирует опасность данной кодировки, которая в последующие годы была признана устаревшей для предотвращения подобных проблем с безопасностью. В настоящее время спецификация HTML явно запрещает использование UTF-7 для предотвращения уязвимостей XSS.
Существует множество других поддерживаемых кодировок символов, большинство из которых на самом деле бесполезны с точки зрения злоумышленника. Все специальные символы HTML, такие как угловые скобки и кавычки, используются только в формате ASCII, и поскольку большинство кодировок символов совместимы с ASCII, то для этих символов нет разницы в используемой кодировке. Даже для UTF-16, который не совместим с ASCII из-за использования двух байт на символ, обычно невозможно эксплуатировать ASCII-символы, поскольку их соответствующее байтовое представление такое же, только с нулевым байтом в конце (little-endian) или в начале (big-endian).
Однако существует особенно интересная кодировка: ISO-2022-JP.
ISO-2022-JP
ISO-2022-JP - это японская кодировка символов, определенная в RFC 1468. Это одна из официальных кодировок символов, которую должны поддерживать пользовательские агенты (браузеры), согласно определению стандарта HTML. Особенно интересным в этой кодировке является то, что она поддерживает определенные escape-последовательности для переключения между различными наборами символов.
Например, если последовательность байтов содержит байты 0x1b, 0x28, 0x42, то эти байты не декодируются в символ, а вместо этого указывают, что все следующие байты должны быть декодированы с использованием кодировки ASCII. В общей сложности существует четыре различных escape-последовательности, которые можно использовать для переключения между наборами символов: ASCII, JIS X 0201 1976, JIS X 0208 1978 и JIS X 0208 1983.
![iso-2022-jp iso-2022-jp](https://habrastorage.org/getpro/habr/upload_files/130/c61/c26/130c61c2687fd166260a1e5f766f5653.png)
Эта особенность стандарта ISO-2022-JP предоставляет не только большую гибкость, но и может нарушить фундаментальные допущения. На момент написания статьи Chrome (Blink) и Firefox (Gecko) автоматически определяли эту кодировку. Появление одной из этих escape-последовательностей обычно достаточно, чтобы алгоритм автоматического определения кодировки считал, что текст HTTP-ответа закодирован в соответствии со стандартом ISO-2022-JP.
В следующих разделах описываются две различные техники эксплуатации, которые могут использовать злоумышленники, когда им удается заставить браузер использовать кодировку ISO-2022-JP. В зависимости от возможностей злоумышленника, это может быть достигнуто, например, путем прямого управления атрибутом charset в заголовке Content-Type или путем вставки тега с помощью уязвимости HTML injection. Если веб-сервер предоставляет недопустимый атрибут charset или не предоставляет его вообще, то обычно не требуется никаких дополнительных условий, поскольку злоумышленники могут легко переключить кодировку на ISO-2022-JP с помощью автоматического определения кодировки.
Метод 1: Устранение экранирования обратной косой черты
Данный метод заключается в том, что контролируемые пользователем данные помещаются в строку JavaScript:
![](https://habrastorage.org/getpro/habr/upload_files/918/aeb/048/918aeb048c0b4ebd05395c3d6d6e9db6.png)
Представим, что есть некий веб-сайт, который принимает два параметра запроса, search и lang. Первый параметр отображается в обычном текстовом контексте, а второй параметр (lang) вставляется в строку JavaScript:
![](https://habrastorage.org/getpro/habr/upload_files/cd1/cac/0e5/cd1cac0e5055b34f9a4d9b93891bb19a.png)
Специальные символы HTML в параметре search закодированы в HTML, а параметр lang обработан путем экранирования двойных кавычек (") и обратной косой черты (). Это не позволит выйти за контекст строки и внедрить JavaScript код:
![](https://habrastorage.org/getpro/habr/upload_files/12b/b9d/c24/12bb9dc24915f5a0dbd475d6a34a6c10.png)
Режимом по умолчанию для стандарта ISO-2022-JP является ASCII. Что означает, что все байты, полученные в теле HTTP ответа, декодируются в ASCII и результирующий HTML документ выглядит так, как мы ожидаем:
![](https://habrastorage.org/getpro/habr/upload_files/9cb/018/d90/9cb018d906520c520f684d380067de5b.png)
Теперь давайте представим, что злоумышленник внедряет в параметр search escape-последовательность для переключения в режим кодировки символов JIS X 0201 1976 (0x1b, 0x28, 0x4a):
![](https://habrastorage.org/getpro/habr/upload_files/3c4/fa6/e59/3c4fa6e598e5160253c3c4c668f4a9fc.png)
Как мы видим, результат содержит все те же символы, что и ранее, поскольку кодировка JIS X 0201 1976 в основном совместима с ASCII. Однако, если мы внимательно изучим его кодовую таблицу, то заметим, что есть два исключения (выделены желтым цветом):
![](https://habrastorage.org/getpro/habr/upload_files/337/06e/5b5/33706e5b598c8ead824de41c8a070111.png)
Байт 0x5c преобразуется в символ йены (¥), а байт 0x7e - в символ надчеркивания (‾). Это отличается от ASCII, где 0x5c сопоставляется с символом обратной косой черты (\), а 0x7e - с символом тильды (~).
Это означает, что когда веб-сервер пытается экранировать двойную кавычку в параметре lang с помощью обратной косой черты, браузер больше не видит обратную косую черту, а вместо нее видит знак йены:
![](https://habrastorage.org/getpro/habr/upload_files/ecc/9fa/80f/ecc9fa80fb84baa920e72ba4510177f5.png)
Соответственно, добавленная двойная кавычка фактически обозначает конец строки и позволяет злоумышленнику внедрить произвольный JavaScript-код:
![](https://habrastorage.org/getpro/habr/upload_files/c8e/c56/1b3/c8ec561b3b1e06964e6634646136c406.png)
Хотя этот способ довольно эффективен, он ограничен обходом санитизации только в контексте JavaScript, поскольку символ обратной косой черты не имеет специального предназначения в рамках HTML. Следующий раздел объясняет более продвинутый способ, который может быть использован в контексте HTML.
Метод 2: Нарушение контекста HTML
Сценарий использования второго метода заключается в том, что злоумышленник может управлять значениями в двух разных контекстах HTML. Наиболее распространенным вариантом использования может быть веб-сайт, поддерживающий разметку markdown. Например, рассмотрим следующий текст markdown:
![blue](0.png) or ![red](1.png)
Результирующий HTML-код выглядит следующим образом:
<img src="0.png" alt="blue"/> or <img src="1.png" alt="red"/>
Важным здесь является то, что злоумышленник может управлять значениями в двух разных контекстах HTML. В данном случае это:
Контекст атрибута (описание изображения/источник)
Обычный текстовый контекст (текст, окружающий изображения)
По умолчанию, ISO-2022-JP использует режим кодировки ASCII и браузер видит HTML-документ, как и ожидалось:
![](https://habrastorage.org/getpro/habr/upload_files/1de/8bc/7af/1de8bc7af2f57cdeb88195834ccf9d81.png)
Теперь предположим, что злоумышленник подставляет escape-последовательность для переключения кодировки на JIS X 0208 1978 в описании первого изображения:
![](https://habrastorage.org/getpro/habr/upload_files/fbf/caf/c18/fbfcafc1849492bd7f4d3b0edc6452cf.png)
Это вынуждает браузер декодировать все следующие байты используя кодировку JIS X 0208 1978. Эта кодировка использует фиксированное количество из двух байт на символ и не совместима с кодировкой ASCII. Это фактически разрушает структуру HTML-документа:
![](https://habrastorage.org/getpro/habr/upload_files/6ad/0eb/327/6ad0eb3275071f72652148fcc3f09c6c.png)
Однако, вторая escape-последовательность может быть добавлена в текстовый контекст между обоими изображениями для переключения кодировки обратно в ASCII:
![](https://habrastorage.org/getpro/habr/upload_files/300/751/c80/300751c8083a7038b2ad50a28c6bba1a.png)
Таким образом, все следующие байты снова декодируются с использованием ASCII:
![](https://habrastorage.org/getpro/habr/upload_files/3b5/e2b/5d1/3b5e2b5d1eb26e8673a4ed038718beba.png)
Однако, изучая код HTML, мы можем заметить, что кое-что изменилось. Начало второго тега img теперь является частью значения атрибута alt:
![](https://habrastorage.org/getpro/habr/upload_files/2ff/c7e/35d/2ffc7e35d31e9c7ae2d82846f1c1eb6e.png)
Причина этого в том, что 4 байта между двумя escape-последовательностями были декодированы с использованием JIS X 0208 1978, тем самым поглотив закрывающие двойные кавычки значения атрибута:
![](https://habrastorage.org/getpro/habr/upload_files/aaa/9bd/4ad/aaa9bd4ad8dd48a89d0ca1d120a76623.png)
К этому моменту значение атрибута src второго изображения, в сущности, больше не является значением атрибута. Таким образом, злоумышленник может заменить это значение обработчиком ошибок JavaScript:
![](https://habrastorage.org/getpro/habr/upload_files/06e/be9/708/06ebe9708d4d1dc0c1bc00d83686ba72.png)
Заключение
В статье мы подчеркнули важность предоставления информации о кодировке символов при работе с HTML-документами. Отсутствие подобной информации может привести к серьезным уязвимостям класса XSS, когда злоумышленники могут изменить набор символов, который обрабатывается браузером.
Мы подробно описали, как браузер определяет набор символов, используемый для декодирования тела HTTP-ответа, и объяснили два различных метода, которые злоумышленники могут использовать для внедрения произвольного кода JavaScript на веб-сайт, используя кодировку символов ISO-2022-JP.
Хотя мы считаем отсутствие указания кодировки символов реальной уязвимостью, автоматическое определение кодировки символов браузером значительно увеличивает ее эффект. Поэтому мы надеемся, что браузеры отключат механизм автоматического определения кодировки в соответствии с нашим предложением - по крайней мере, для кодировки символов ISO-2022-JP.
qw1
Вообще странно что причиной проблемы тут указывается отсутствие encoding.
Настоящая причина - весь пользовательский ввод, отображающийся в HTML, должен проходить через escape-фунцию, которая заменит
<script>
на< script >
, а коды типа 0x1b на
. Если так сделать, то и отсутствие encoding не позволит что-то эксплуатировать.Просто добавление encoding не закрывает XSS-инъекции, при отсутствии экранирования пользовательского ввода. То есть, слона то вы и не заметили.
DarthVictor
Вы много знаете escape-фунций, которые чистят ввод в японской кодировке? С точки зрения стандартной escape-фунции ввод абсолютно чист. В нём символ йены и пара иероглифов.
qw1
На SO этот вопрос разобран (к сожалению, ответ не помечен как верный)
Dren0r
По-моему, все топ фреймворки имеют санитайзеры из коробки, вставкой квери параметров напрямую в джс можно удивить студента на 1 курсе. В реальных же проектах все делается само и никакие экранирования не нужны.
Переменные из юрл отобразятся просто как текст, а не рендер html/js.