Какие предположения можно сделать относительно следующего HTTP ответа сервера?

Глядя на этот небольшой фрагмент HTTP ответа, можно предположить, что веб-приложение, вероятно, содержит уязвимость XSS.

Почему это возможно? Что обращает на себя внимание в этом ответе сервера?

Вы будете правы, если сомневаетесь насчет заголовка Content-Type. В нем есть незначительный недостаток - отсутствие атрибута charset.  Это может казаться неважным, однако, в этой статье мы объясним, как злоумышленники могут использовать этот недостаток для внедрения произвольного JavaScript кода на веб-сайт, сознательно изменяя набор символов, который ожидает браузер.

Кодировка символов

Типичный заголовок Content-Type в HTTP ответе выглядит следующим образом:

Атрибут charset сообщает браузеру, что для кодирования тела HTTP ответа использовалась кодировка UTF-8. Кодировка символов, такая как UTF-8, определяет соответствие между символами и байтами. Когда веб-сервер обрабатывает HTML документ, он соотносит символы документа с соответствующими байтами и передает их в теле HTTP ответа. Этот процесс преобразует (кодирует) символы в байты:

Когда браузер получает эти байты в теле HTTP ответа, он может преобразовать их обратно (декодировать) в символы HTML документа:

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-документа, упорядоченных по приоритету:

  1. Метка порядка байтов в начале HTML документа,

  2. атрибут charset в заголовке Content-Type,

  3. тэг <meta> в HTML документе.

Отсутствие информации о кодировке

Метка порядка байтов обычно используется очень редко, а атрибут charset не всегда присутствует в заголовке Content-Type или может быть указан неверно. Кроме того, особенно для частичных HTML-ответов от сервера, обычно отсутствует тег <meta>, указывающий кодировку символов. В этих случаях браузер не располагает никакой информацией о том, какой набор символов следует использовать:

Вы когда-нибудь видели подобное сообщение об ошибке? Вероятно, нет, потому что его не существует.

Как и в случае с неправильным синтаксисом HTML, браузеры пытаются восстановить недостающую информацию о наборе символов при анализе полученного от веб-сервера контента, и извлечь из этого максимум возможного. Такое нестрогое поведение способствует хорошему взаимодействию с пользователем, но также может открыть возможности для использования таких техник эксплуатации, как mXSS.

При отсутствии информации о кодировке символов браузеры пытаются сделать предположение, основываясь на содержимом, что называется автоматическим определением (auto-detection). Это похоже на поиск по типу MIME (MIME-type sniffing), но работает на уровне кодировки символов. Например, движок рендеринга Chromium Blink использует библиотеку Compact Encoding Detection (CED) для автоматического определения кодировки символов. Как мы увидим дальше, с точки зрения злоумышленника, функция автоматического определения кодировки символов является очень мощным инструментом.

На данный момент мы познакомились с различными механизмами, которые используют браузеры для определения кодировки символов HTML документов. Но как злоумышленники могут это использовать?

Различия в кодировках

Цель кодирования символов - преобразовать символы в последовательность байтов, обрабатываемую компьютером. Эти байты передаются по сети и декодируются обратно в символы получателем. Таким образом, восстанавливаются все те же символы, которые хотел передать отправитель:

Это работает хорошо только в том случае, когда отправитель и получатель согласовали способ кодировки символов. Если есть несоответствие между кодировкой символов, используемой для кодирования и декодирования, то получатель может увидеть разные символы:

Подобное несоответствие между кодировкой символов, используемой для кодирования и декодирования, и есть то, что мы называем различием в кодировках в этой статье.

Для веб-приложений жизненно важно, когда контролируемые пользователем данные очищаются для предотвращения уязвимостей Cross-Site Scripting (XSS). Если кодировка символов, используемая браузером, отличается от той, которую использует веб-сервер, это теоретически может нарушить очистку данных и привести к уязвимостям XSS.

Само по себе это не является большой новостью, и даже Google сталкивался с подобной проблемой еще в 2005 году. Страница Google 404 не предоставляла информации о кодировке символов, что могло быть проэксплуатировано через добавление полезной нагрузки XSS в кодировке UTF-7, В UTF-7 специальные символы HTML, такие как угловые скобки, кодируются иначе, чем в ASCII, что может быть использовано для обхода процедуры санитизации:

Это наглядно демонстрирует опасность данной кодировки, которая в последующие годы была признана устаревшей для предотвращения подобных проблем с безопасностью. В настоящее время спецификация 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

Эта особенность стандарта 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:

Представим, что есть некий веб-сайт, который принимает два параметра запроса, search и lang. Первый параметр отображается в обычном текстовом контексте, а второй параметр (lang) вставляется в строку JavaScript:

Специальные символы HTML в параметре search закодированы в HTML, а параметр lang обработан путем экранирования двойных кавычек (") и обратной косой черты (). Это не позволит выйти за контекст строки и внедрить JavaScript код:

Режимом по умолчанию для стандарта ISO-2022-JP является ASCII. Что означает, что все байты, полученные в теле HTTP ответа, декодируются в ASCII и результирующий HTML документ выглядит так, как мы ожидаем:

Теперь давайте представим, что злоумышленник внедряет в параметр search escape-последовательность для переключения в режим кодировки символов JIS X 0201 1976 (0x1b, 0x28, 0x4a):

Как мы видим, результат содержит все те же символы, что и ранее, поскольку кодировка JIS X 0201 1976 в основном совместима с ASCII. Однако, если мы внимательно изучим его кодовую таблицу, то заметим, что есть два исключения (выделены желтым цветом):

Байт 0x5c преобразуется в символ йены (¥), а байт 0x7e - в символ надчеркивания (‾). Это отличается от ASCII, где 0x5c сопоставляется с символом обратной косой черты (\), а 0x7e - с символом тильды (~).

Это означает, что когда веб-сервер пытается экранировать двойную кавычку в параметре lang с помощью обратной косой черты, браузер больше не видит обратную косую черту, а вместо нее видит знак йены:

Соответственно, добавленная двойная кавычка фактически обозначает конец строки и позволяет злоумышленнику внедрить произвольный JavaScript-код:

Хотя этот способ довольно эффективен, он ограничен обходом санитизации только в контексте 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-документ, как и ожидалось:

Теперь предположим, что злоумышленник подставляет escape-последовательность для переключения кодировки на JIS X 0208 1978 в описании первого изображения:

Это вынуждает браузер декодировать все следующие байты используя кодировку JIS X 0208 1978. Эта кодировка использует фиксированное количество из двух байт на символ и не совместима с кодировкой ASCII. Это фактически разрушает структуру HTML-документа:

Однако, вторая escape-последовательность может быть добавлена в текстовый контекст между обоими изображениями для переключения кодировки обратно в ASCII:

Таким образом, все следующие байты снова декодируются с использованием ASCII:

Однако, изучая код HTML, мы можем заметить, что кое-что изменилось. Начало второго тега img теперь является частью значения атрибута alt:

Причина этого в том, что 4 байта между двумя escape-последовательностями были декодированы с использованием JIS X 0208 1978, тем самым поглотив закрывающие двойные кавычки значения атрибута:

К этому моменту значение атрибута src второго изображения, в сущности, больше не является значением атрибута. Таким образом, злоумышленник может заменить это значение обработчиком ошибок JavaScript:

Заключение

В статье мы подчеркнули важность предоставления информации о кодировке символов при работе с HTML-документами. Отсутствие подобной информации может привести к серьезным уязвимостям класса XSS, когда злоумышленники могут изменить набор символов, который обрабатывается браузером.

Мы подробно описали, как браузер определяет набор символов, используемый для декодирования тела HTTP-ответа, и объяснили два различных метода, которые злоумышленники могут использовать для внедрения произвольного кода JavaScript на веб-сайт, используя кодировку символов ISO-2022-JP.

Хотя мы считаем отсутствие указания кодировки символов реальной уязвимостью, автоматическое определение кодировки символов браузером значительно увеличивает ее эффект. Поэтому мы надеемся, что браузеры отключат механизм автоматического определения кодировки в соответствии с нашим предложением - по крайней мере, для кодировки символов ISO-2022-JP.

Комментарии (4)


  1. qw1
    10.08.2024 22:43
    +5

    Вообще странно что причиной проблемы тут указывается отсутствие encoding.
    Настоящая причина - весь пользовательский ввод, отображающийся в HTML, должен проходить через escape-фунцию, которая заменит <script> на &lt; script &gt;, а коды типа 0x1b на &#27;. Если так сделать, то и отсутствие encoding не позволит что-то эксплуатировать.

    Просто добавление encoding не закрывает XSS-инъекции, при отсутствии экранирования пользовательского ввода. То есть, слона то вы и не заметили.


    1. DarthVictor
      10.08.2024 22:43
      +2

      Вы много знаете escape-фунций, которые чистят ввод в японской кодировке? С точки зрения стандартной escape-фунции ввод абсолютно чист. В нём символ йены и пара иероглифов.


      1. qw1
        10.08.2024 22:43
        +3

        На SO этот вопрос разобран (к сожалению, ответ не помечен как верный)

        OWASP recommends that "[e]xcept for alphanumeric characters, [you should] escape all characters with ASCII values less than 256 with the &#xHH; format (or a named entity if available) to prevent switching out of [an] attribute." So here's a function that does that, with a usage example:

        function escapeHTML(unsafe) {
          return unsafe.replace(
            /[\u0000-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u00FF]/g,
            c => '&#' + ('000' + c.charCodeAt(0)).slice(-4) + ';'
          )
        }
        


      1. Dren0r
        10.08.2024 22:43
        +1

        По-моему, все топ фреймворки имеют санитайзеры из коробки, вставкой квери параметров напрямую в джс можно удивить студента на 1 курсе. В реальных же проектах все делается само и никакие экранирования не нужны.
        Переменные из юрл отобразятся просто как текст, а не рендер html/js.