Разбор задания 1 второй половины дня экзамена специалиста по обеспечению информационной безопасности, осень 5-го года Рэйва (2023) — хранимый XSS, из-за которого из 16 отзывов видно только 2
· Обновлено: · Го Комура · Специалист по обеспечению информационной безопасности, Экзамен RISS, XSS, Межсайтовый скриптинг, Веб-приложение, Информационная безопасность, Уязвимости, IPA, Управление сеансами
«Отзывов должно быть 16, а показываются только 2.»
Задание 1 второй половины дня экзамена специалиста по обеспечению информационной безопасности (осенняя сессия 5-го года Рэйва, 2023) начинается с этого обращения пользователя1. С переходами между экранами всё в порядке, ошибок тоже нет. Просто число отображаемых записей не сходится.
Причина — хранимый межсайтовый скриптинг (XSS). Но интересно в этой задаче не сам ответ «XSS». Интересно то, что все «разумные» меры, которые компания Q (интернет-магазин из условия задачи) уже приняла, были обойдены. Ограничение заголовка отзыва в 50 символов обошли, токен, необходимый для загрузки, получили штатным порядком, а похищенный идентификатор сеанса вынесли наружу, ни разу не отправив на внешний сервер.
Эта статья разбирает случай в таком порядке: «что произошло → какие следы становятся основанием для ответа → какая мера позволила бы это остановить». Помимо названия XSS, она разбирает, где проходит граница между мерами, которые не сработали, и мерами, которые должны были сработать.
Рассматриваются образцы ответов экзамена и их обоснование, а также общая картина мер против XSS, применимых на практике. Выберите точку входа, подходящую вашей цели.
| Зачем вы читаете | Что открыть первым | Как читать дальше |
|---|---|---|
| Хочется решить задание заново | Таблица соответствия пунктов в главе 2 | Открыть буклет с заданиями и проверить образцы ответов и их обоснование в главах 4–8 |
| Хочется понять, почему 16 отзывов выглядят как 2 | Симптом в главе 3 | Разобраться в разнице между отображением и HTML, а затем перейти к раздельной публикации в главе 5 |
| Хочется увидеть весь путь выноса данных | Диаграмма последовательности в главе 7 | Отдельно проследить публикацию, выполнение на стороне жертвы и сбор на стороне атакующего |
| Хочется применить это к мерам или проверкам на работе | Сравнение мер в главе 9, пункты проверки в главе 10 | Разделить устранение причины и компенсирующие меры, проверить область действия и границы каждой |
В главах 4–8 образцы ответов — это ответы экзамена, а CSP и перекодирование изображений — дополнение, расширяющее тему до практики. В главе 8 особенно чётко разделяются условия, записанные в условии задачи, и условия, которые действуют при изменении настройки.
Общий вопрос о том, по каким критериям проверять безопасность сайта целиком, разобран в статье Заказчику сайта тоже стоит знать: как пользоваться документом IPA «Как создать безопасный веб-сайт». Настоящая статья углубляется с практическим примером в одну из 11 уязвимостей, перечисленных там, — XSS.
1. Сначала выводы
Что произошло
- Тип уязвимости — хранимый XSS. Строка атакующего сохранилась на сервере, и с этого момента она выводилась в HTML каждому, кто открывал страницу. Использование внедрённым скриптом DOM API и принадлежность случая к DOM Based XSS — разные вопросы
- Ограничение числа символов во вводе мерой против XSS не стало. Атакующий разбил публикацию на 15 частей и заставил HTML, попадающий между публикациями, пропускаться как комментарий JavaScript, соединив фрагменты в один скрипт
- Токен для загрузки (мера против CSRF) тоже не стал мерой против XSS. Атакующий скрипт получает токен тем же порядком, что и штатный экран. Пока он выполняется на том же origin, ему доступно всё, что доступно легальному пользователю
- Похищенный идентификатор сеанса наружу не ушёл. Содержимое cookie положили в собственную функцию загрузки иконок сайта как файл изображения с именем «a.png», и атакующий забрал его, просто просмотрев. Средства контроля исходящего трафика этого не обнаруживают
Где это можно было остановить
- В условии задачи прямо названы три вещи, которых не хватало компании Q: экранирование при выводе (устранение причины), а также атрибут HttpOnly у cookie и проверка формата загружаемых файлов (эти две — компенсирующие меры). Достаточно было любой одной из них, чтобы цепочка атаки прервалась. Правда, проверку формата можно обойти, сменив приём, поэтому нужна ещё и перекодировка
- Хотя в условии задачи этого нет,
script-srcв CSP тоже способен разорвать ту же цепочку. При конфигурации, не разрешающей'unsafe-inline', внедрённый встроенный скрипт вообще не выполняется
«Срабатывает» здесь означает способность остановить цепочку атаки из этой задачи. Один только HttpOnly или перекодирование изображений не устраняет сам XSS. В главе 9 мы разберём и границы — что происходит, когда приём меняется.
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 25, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. О материале — источник и как он подан в статье
Разбирается следующее задание.
Источник: 5-й год Рэйва (2023), осенняя сессия, экзамен специалиста по обеспечению информационной безопасности, вторая половина дня, задание 1
IPA указывает, что для публикуемых ею прошлых экзаменационных заданий разрешение и плата не требуются, кроме случаев, специально оговорённых законом. Это не означает отказа от авторских прав: IPA требует указывать источник в форме «год, сессия, категория экзамена, временной интервал, номер задания и т. д.», а также отдельно отмечать, если часть задания была изменена2.
В этой статье HTML и скрипты из буклета с заданиями не воспроизводятся дословно. В объёме, необходимом для объяснения механизма, они заменены эквивалентными примерами кода, написанными нашей компанией. Формулировки пунктов и образцы ответов также приводятся в сжатом виде. Оригиналы буклета с заданиями, образцов ответов и комментариев по оценке можно бесплатно скачать со страницы IPA, и мы советуем читать статью, держа их открытыми1 3 4.
Соответствие пунктов задания и разделов статьи
Для тех, кто читает с открытым буклетом с заданиями, ниже показано соответствие пунктов задания и разделов этой статьи. Начинать можно с того пункта, который хочется решить.
| Пункт | Что спрашивают (число символов) | Соответствующий раздел статьи |
|---|---|---|
| Пункт 1(1) | Тип уязвимости XSS, которая была использована (выбор из 3) | Глава 4 |
| Пункт 1(2) | Мера в веб-приложении Q (не более 30 символов) | Глава 4, «Пункт 1(2): мера» |
| Пункт 2 | Способ, которым заставили выполниться скрипт длиннее ограничения на число символов (не более 50 символов) | Глава 5 |
| Пункт 3(1) | Что делают строки с 6-й по 20-ю атакующего скрипта (не более 60 символов) | Глава 6 |
| Пункт 3(2) | Как атакующий получает загруженную информацию (не более 50 символов) | Глава 7 |
| Пункт 3(3) | Что можно сделать с полученной информацией (не более 40 символов) | Глава 7, «Пункт 3(3): что можно сделать с идентификатором сеанса» |
| Пункт 4 | Механизм браузера, из-за которого атака не удаётся на домене атакующего (не более 40 символов) | Глава 8 |
Тем, кому нужна только практическая часть без пунктов задания, стоит начать с главы 9 (список сработавших и несработавших мер) и главы 10 (точки проверки в код-ревью). Если хочется увидеть саму цепочку атаки на одной странице, диаграмма последовательности в главе 7 даёт полную картину от публикации до сбора данных.
Соответствие текста буклета и примеров в этой статье
Чтобы можно было сверить с оригиналом, ниже собрано, что и как было заменено.
| Текст в буклете с заданиями | Как это подано в статье | Где находится |
|---|---|---|
| HTML страницы V (включая заголовки отзывов, опубликованные по частям) | Не воспроизводится дословно; написан эквивалентный пример, сжимающий тот же механизм до трёх публикаций | Глава 5 |
| Извлечённый атакующий скрипт (около 20 строк) | Не воспроизводится дословно; написан эквивалентный JavaScript, выполняющий ту же обработку. Комментарии в коде — для объяснения в этой статье | Глава 6 |
| Формулировка каждого пункта | Сжатое изложение с сохранением сути (условия вроде ограничений на число символов — в исходных значениях) | Начало каждой из глав 4–8 |
| Образцы ответов | Образцы ответов, опубликованные IPA3 | По главам 4–8 |
| Комментарии по оценке | Соответствующие места из опубликованных IPA комментариев по оценке4 | Главы 4, 5 и 7 |
| Спецификации из условия задачи (компания Q, страница V, участники A и B, функции и ограничения на число символов и т. д.) | Сжатое изложение по тексту оригинала | Глава 2, «Место действия задачи» |
Место действия задачи
Речь идёт о компании Q — интернет-магазине одежды со штатом 100 человек. Она ведёт магазин на собственной разработке, «веб-приложении Q», и пользователи обращаются к нему по HTTPS. По условию, компания как раз добавила функцию отзывов на товары для участников.
Запомнить нужно пять пунктов спецификации.
| Функция | Спецификация |
|---|---|
| Вход в систему | Аутентификация по идентификатору участника и паролю, выдача идентификатора сеанса в виде cookie |
| Отзывы на товары | Публиковать могут только участники, вошедшие в систему. Для заголовка отзыва действует ограничение в 50 символов, для текста отзыва — в 300 символов, оба поля свободные |
| Профиль участника | Есть страница загрузки иконки и страница регистрации данных банковской карты. Обе доступны только участникам, вошедшим в систему |
| Загрузка иконки | Отправляет файл изображения и токен как параметры в /user/upload. Успешно только тогда, когда токен совпадает с выданным в /user/profile |
| Отображение иконки | Загруженная иконка показывается на странице настроек профиля участника и на страницах отзывов |
Последние две строки — подсказка для размышления о том, что нужно, чтобы загрузить, и кто может прочитать загруженное. Получение токена разбирается в главе 6, а сбор данных через изображение — в главе 7.
3. Симптом — почему 16 превратились в 2
Сначала сравним число на экране с числом в HTML
От участника приходит обращение: на странице отзывов о футболке без принта (страница V) из 16 отзывов, которые должны отображаться, показываются только 2. Когда господин N из отдела разработки открывает страницу V, на экране видно следующее.
- В заголовке — «16 отзывов»
- Один отзыв участника A (заголовок «Good», текст «Nice shirt!»)
- Один отзыв участника B
- В конце — «Всё, все 16 отзывов»
Число в счётчике — 16, а фактически размещено 2. Если на этом месте посмотреть HTML, обнаружится, что публикаций участника A — 15, и среди них внедрён длинный скрипт.
Иначе говоря, сами отзывы выведены в HTML все 16. И всё же видно только 2, потому что большая их часть оказалась содержимым элемента <script>, и браузер перестал считать их содержимым для отображения.
Остаются видны начало первой публикации и конец пятнадцатой
Поглощаемый диапазон — если точно, не «все 15 целиком». Открывающий тег <script> появляется в середине заголовка первого отзыва (сразу после «Good»), а закрывающий </script> стоит в конце заголовка пятнадцатого отзыва. Поэтому:
- Карточка первой публикации (иконка, отображаемое имя, дата, ★ и заголовок до «Good») стоит перед
<script>, значит отображается - Всё от неё до конца заголовка пятнадцатого поглощается как содержимое элемента script
- Остаток пятнадцатой публикации (текст «Nice shirt!») идёт после
</script>, значит отображается
В результате верхняя часть первой публикации соединяется с текстом пятнадцатой, и на экране это выглядит как «один отзыв участника A с заголовком Good и текстом Nice shirt!». Прибавляем один отзыв участника B — получаем 2. Атакующий, видимо, поставил в начале первой публикации «Good», а в тексте пятнадцатой — «Nice shirt!», естественные строки, чтобы сломанная вёрстка не выглядела подозрительно.
Умение читать такой симптом — «число верное, а отображение нет» — пригодится и на практике. Это точка входа в подозрение, что дело не в ошибке логики подсчёта, а в том, что сломана структура выведенного HTML.
4. Почему XSS «хранимый» — задание 1
Пункт 1(1): ответ и следы, по которым он получен
В пункте 1 нужно выбрать тип уязвимости XSS, использованной в этой атаке, из трёх вариантов: DOM Based XSS, хранимый XSS и отражённый XSS. Правильный ответ — хранимый XSS.3
Обоснование — опубликованная строка сохранилась на сервере и была выведена в HTML как есть. Не выбирайте тип по названиям API, которые скрипт использует после запуска.
А про этот пункт комментарии по оценке IPA пишут так.4
Доля правильных ответов была средней, но встречались экзаменуемые, ошибочно отвечавшие «DOM Based XSS», возможно, потому что скрипт использовал DOM.
Атакующий скрипт из условия задачи использует XMLHttpRequest, принимает ответ как DOM и достаёт элемент через getElementById. Он действительно работает с DOM. Однако это не имеет отношения к типу уязвимости.
Граница проходит там, где строка атаки становится скриптом
Три категории разделяет то, где строка атаки превращается в исполняемый код, то есть находится ли уязвимое место вывода (sink) на стороне сервера или на стороне браузера.
| Тип | Sink (место, где строка атаки становится исполняемой) | Откуда приходит строка атаки |
|---|---|---|
| Отражённый XSS | Обработка, в которой сервер собирает HTML | Параметр URL, подготовленного атакующим, то есть сам этот запрос |
| Хранимый XSS | Обработка, в которой сервер собирает HTML | Данные, сохранённые на стороне сервера |
| DOM Based XSS | JavaScript в браузере (присваивание в innerHTML, eval, document.write и т. п.) |
Фрагмент URL, postMessage, значение, полученное от сервера, и т. п. |
В этой задаче сервер вставил заголовок отзыва, опубликованный атакующим, в HTML как есть и вернул его. Sink находится в серверной обработке вывода, а строка сохраняется в базе данных и затем раздаётся всем, кто с этого момента открывает страницу V. Значит, это хранимый тип.
flowchart TD
A["Строка атакующего выполнилась<br/>как скрипт"] --> B{"Где она стала<br/>исполняемой"}
B -->|"Когда JS в браузере<br/>передал её<br/>в innerHTML и т. п."| C["DOM Based XSS"]
B -->|"Она была в HTML,<br/>который собрал сервер"| D{"Эта строка<br/>сохранена<br/>на сервере"}
D -->|"Сохранена<br/>(далее выводится всем)"| E["Хранимый XSS"]
D -->|"Не сохранена<br/>(только для этого запроса)"| F["Отражённый XSS"]
Упрощение «если строка атаки попала в ответ сервера, значит это хранимый или отражённый тип» опасно. Даже если сервер возвращает значение как безобидный текст или JSON, оно станет DOM Based XSS, если JavaScript в браузере присвоит его в innerHTML (так называемый stored DOM XSS, где причиной становится сохранённое значение). В этом случае чинить нужно не серверную обработку вывода, а sink на стороне клиента, поэтому судите не по тому, видна ли строка в ответе, а по тому, где она стала исполняемой.
Что делает внедрённый скрипт (работает с DOM, обменивается данными, читает cookie) и как этот скрипт попал на страницу нужно рассматривать отдельно. Путать их мешает не выбору меры, а выбору неверной меры. Решив, что это DOM Based XSS, вы придёте к выводу «достаточно починить JavaScript на стороне клиента», тогда как чинить нужно было серверную обработку вывода.
Пункт 1(2): мера
В пункте 1(2) нужно ответить, какова мера в веб-приложении Q, не более чем в 30 символов.
Образец ответа: «Применять экранирование к заголовку отзыва перед выводом».3
Обоснование ответа: чинить нужно не экран, принимающий публикацию, а обработку, которая вставляет сохранённый заголовок отзыва в HTML.
Здесь важно, что явно назван порядок — «перед выводом». Экранирование выполняется не тогда, когда принят ввод, а непосредственно перед выводом в HTML, по контексту места вывода. В документе IPA «Как создать безопасный веб-сайт» первым среди мер по устранению причины XSS тоже указано «применять экранирование ко всем элементам, выводимым на веб-страницу»5.
«Разве для SQL-инъекции важно экранирование ввода?»
Дальше идёт дополнение, расширяющее «перед выводом» из пункта 1(2) до практики. Читайте его отдельно от самого образца ответа.
Если сразу перейти к выводу: и для SQL-инъекции «экранирование при вводе» мерой не является.
Как устранение причины SQL-инъекции IPA называет «реализовать всю сборку SQL-запросов через плейсхолдеры». Экранирование упомянуто как альтернатива для случая, когда SQL-запрос приходится собирать конкатенацией строк, но и там сказано: «если сборка SQL-запроса выполняется конкатенацией строк, используйте API СУБД, выполняющий экранирование, чтобы правильно составить литералы SQL-запроса» — то есть экранирование делается в момент сборки SQL-запроса6. Не в момент получения ввода.
Структура получается точно такой же, как в случае HTML. Общий принцип таков.
Экранирование выполняется там, где решается, в мир какой грамматики уходят данные.
- Если данные уходят как HTML — экранирование HTML
- Если становятся частью SQL-запроса — плейсхолдеры (а если иначе никак, экранирование как литерал SQL)
- Если становятся частью команды оболочки — обработка по правилам оболочки
Обрабатывать не при вводе, а там, где определён приёмник вывода
Почему не при вводе? Потому что в момент получения ввода ещё не решено, куда эти данные уйдут. Один и тот же текст отзыва уходит и на HTML-страницу, и в экспорт CSV, и в JSON-ответ API, и в письмо-уведомление, и в лог. Если применить экранирование HTML при вводе, строка & попадёт в CSV как есть, содержимое базы данных станет отличаться от исходного ввода, и поиск с агрегацией начнут давать неверные результаты. Это ещё и питательная среда для двойного экранирования.
Значит, на стороне ввода делать нечего?
Нет. Но на стороне ввода выполняется не экранирование, а проверка (валидация). Это разные роли.
- Отсеивать при вводе — не принимать значения, невозможные по спецификации. Для поля почтового индекса отклонять всё, кроме семи цифр; для поля количества — отрицательные числа. Это независимо необходимая обработка, охраняющая корректность данных
- Экранировать при выводе — безопасно представить принятые данные в грамматике места, куда они уходят. Это и есть устранение причины уязвимости
И важно не возлагать на проверку ввода слишком большие надежды как на меру против XSS. Говоря о XSS, IPA упоминает способ проверять, соответствует ли введённое значение спецификации приложения, и прямо указывает, что эффективность этой меры ограничена и если спецификация приложения допускает ввод широкого набора символов, мерой она не является, поэтому полагаться на этот способ не рекомендуется5.
Заголовок и текст отзыва в этой задаче были именно такими: свободные поля, допускающие широкий набор символов. Верхние границы в 50 и 300 символов есть, но символов, нужных для запуска скрипта, куда меньше, так что само наличие верхней границы защитой не является. Что в действительности произошло в компании Q, разбирается в следующем пункте 2.
5. Как обошли ограничение в 50 символов — задание 2
Пункт 2 — самое интересное в этом задании.
По рисунку 3 объясните не более чем в 50 символах, каким способом заставили выполниться скрипт длиннее ограничения на число символов.
Образец ответа: «Публикацию провели в несколько приёмов так, что HTML закомментировался и превратился в один скрипт».3
В ответ нужно включить не только несколько публикаций, но и механизм, который пропускает промежуточный HTML как комментарий. Разберём в три шага.
Смотреть по порядку: публикация, HTML, JavaScript
Атакующий разбил нагрузку на длины, укладывающиеся в одну публикацию, и опубликовал 15 раз. Важно, как поступить с HTML, который неизбежно встаёт между публикациями (</div>, <div class="..."> и т. п.). Для JavaScript это синтаксическая ошибка.
Для этого использовался блочный комментарий JavaScript. Ниже — упрощённый вариант, написанный нашей компанией для объяснения механизма (это не цитата рисунка из буклета с заданиями).
1. Публикуемые строки
Предположим, в поле заголовка отзыва опубликовали так, в три приёма.
Публикация 1: замечательно<script>a=1;/*
Публикация 2: */b=2;/*
Публикация 3: */c=3;</script>
2. HTML, который собирает сервер
Сервер выводит это в HTML как заголовок каждого отзыва, вот так.
<div class="review-title">замечательно<script>a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;</script></div>
3. JavaScript, который выполняет браузер
С точки зрения браузера, от первого <script> до последнего </script> — один элемент script. Его содержимое получается таким.
a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;
HTML между /* и */ пропускается как комментарий JavaScript, и фактически выполняется только a=1; b=2; c=3;. А пропущенная как комментарий часть, будучи содержимым элемента script, на экране тоже не отображается. В этом и суть симптома «16 выглядят как 2».
Что отсюда следует вынести
Не перепутайте здесь причину и следствие. Ограничение числа символов не становится мерой против XSS потому, что число публикаций было неограниченным. Потому что 50 символов и так достаточно для выполнения скрипта. Добавить один атрибут обработчика события или поставить один тег, подгружающий внешний скрипт, — и в обоих случаях хватит нескольких десятков символов. Даже если ограничить публикацию одной записью, ограничение числа символов защитой не станет.
А чем была раздельная публикация в этом случае? Способом принести целиком длинный скрипт примерно на 20 строк. Атакующему нужно было много, и он набрал это количеством, — но это не сама причина того, что ограничение было обойдено. Описание приёма и оценку его как меры нужно держать порознь.
То же самое относится к любым ограничениям на стороне ввода.
- Ограничения на число символов, на допустимый набор символов, валидация на фронтенде — всё это требуется спецификацией, но заменой устранению причины XSS не является
- У атакующего всегда есть пространство для обхода ограничений на стороне ввода: разбиение, кодирование, подача через другой путь
- Защищать нужно момент, когда данные уходят как HTML
Комментарии по оценке: не считать, что ограничение сняли
Комментарии по оценке IPA об этом пункте говорят и так: встречались ответы, в которых проверка представляется недостаточной, например «атакующий удалил ограничение на ввод инструментами разработчика и опубликовал». Действительно, ограничения фронтенда можно снять инструментами разработчика. Но в этом пункте требовалось прочитать по следам, оставшимся в HTML, что было сделано в действительности. В HTML рисунка 3 было 15 отдельных публикаций, каждая укладывалась в ограничение, и в начале и конце каждой стояли символы комментария. Это следы не «снятия» ограничения, а его «обхода». Это следует читать как проверку готовности шаг за шагом убеждаться в том, что оставил атакующий.
Важность такого чтения отмечается и в комментариях по оценке.4
Мысли о том, как проектировать проверку входных значений — где и в каком объёме, — изложены как проектирование, не доверяющее данным извне, и в статье Не используйте значение, считанное с QR-кода, как есть.
6. Что делал скрипт — задание 3(1)
Скрипт, который извлёк господин N, занимает около 20 строк. В пункте 3(1) нужно ответить не более чем в 60 символах, что делают строки с 6-й по 20-ю (часть, которая выполняется после успешного первого обращения).
Образец ответа: «Загружает идентификатор сеанса как иконку, вместе с токеном, полученным из ответа XHR».3
Порядок чтения таков: получить токен → превратить строку cookie в файл → загрузить, приложив токен. Если разделить, что, чем и куда отправлено, становятся видны ключевые точки ответа.
Ниже — эквивалентный код, написанный нашей компанией для объяснения этого поведения (это не цитата рисунка из буклета с заданиями). «Строки с 6-й по 20-ю» в пункте — это номера строк в буклете с заданиями, а не в приведённом ниже примере. В примере комментарий ① — подготовка, комментарии ②–④ соответствуют обработке, о которой спрашивает пункт.
// ① Сначала получаем страницу настроек профиля
const xhr = new XMLHttpRequest();
xhr.open("get", "https://example.jp/user/profile");
xhr.responseType = "document"; // принимаем ответ не как текст, а как DOM
xhr.send();
xhr.onload = function () {
// ② тем же порядком, что и штатный экран, читаем токен для загрузки
const token = xhr.response.getElementById("token").value;
// ③ содержимое cookie (вместе с идентификатором сеанса) делаем содержимым PNG-файла
const file = new File([document.cookie], "a.png", { type: "image/png" });
// ④ отправляем в собственную функцию загрузки иконок сайта
const form = new FormData();
form.append("uploadfile", file);
form.append("token", token);
const xhr2 = new XMLHttpRequest();
xhr2.open("post", "https://example.jp/user/upload");
xhr2.send(form);
};
Первые пять строк нужны только чтобы пройти меру против CSRF у компании Q
Что здесь стоит внимания — вся первая половина целиком посвящена получению токена.
Компания Q при загрузке иконки требовала, чтобы токен совпадал с выданным в /user/profile. Это механизм, не дающий загрузить что-либо из поддельной формы, размещённой на другом сайте, то есть мера против CSRF.
Однако атакующий скрипт работает внутри браузера жертвы, на том же origin, что и жертва. Значит, достаточно сделать то же, что делает штатный экран. GET страницы настроек профиля и чтение значения элемента token в ней. Уже этого хватает, чтобы получить токен.
Токен против CSRF не защищает от XSS. Как только XSS состоялся, код атакующего может вести себя как вошедший в систему легальный пользователь. Токен защищает от запросов, приходящих самовольно с другого origin, а не от вредоносного скрипта, работающего на том же origin.
Это различие полезно и при чтении требований по безопасности на работе. Объяснение «мы ставим CSRF-токен, поэтому всё в порядке» верно в отношении CSRF, но об XSS не говорит ничего.
Что означает объект File
Строка ③ тоже примечательна. Создаётся файловый объект, содержимым которого является строка document.cookie, именем файла — a.png, а MIME-типом — image/png.
Содержимое — обычный текст. Это не PNG-файл. Тем не менее загрузка прошла, потому что, как сказано в условии, веб-приложение Q не проверяло формат загружаемого файла изображения.
Почему идентификатор сеанса удалось прочитать и какую область защищает HttpOnly
А document.cookie удалось прочитать потому, что на cookie не был установлен атрибут HttpOnly. RFC 6265 определяет атрибут HttpOnly так: он ограничивает область действия cookie HTTP-запросами, и в частности предписывает пользовательскому агенту опускать cookie при предоставлении доступа к cookie через «не-HTTP» API, например через API веб-браузера, открывающее cookie скриптам7. Будь этот атрибут установлен, из строки, которую захватывает строка ③, выпал бы именно идентификатор сеанса, и выносить было бы нечего.
Здесь важно понимать, что HttpOnly скрывает только те cookie, на которых установлен этот атрибут. Если на том же origin есть cookie без HttpOnly (настройки отображения, идентификаторы для аналитики и т. п.), document.cookie продолжит их возвращать. «Если поставить HttpOnly, document.cookie станет пустым» — неверно. Защитить нужно идентификатор сеанса, поэтому проверьте отдельно, что на cookie с идентификатором сеанса он точно установлен.
7. Вынос данных, который не выходит наружу — задание 3(2)(3)
В пункте 3(2) спрашивают, как атакующий может получить загруженную информацию, не более чем в 50 символах.
Образец ответа: «Скачать иконку участника и извлечь из неё строку идентификатора сеанса».3
Основание для сбора — место отображения после загрузки
Вспомните, что было в спецификации из условия задачи.
Загруженная иконка показывается на странице настроек профиля участника и на страницах отзывов.
Значит, «иконка», в которую записан идентификатор сеанса жертвы, размещается в доступном для просмотра месте на сайте. Атакующему не нужно ничего особенного. Открыть страницу, где отображается иконка жертвы, и получить URL этого изображения. Содержимое — текст, так что при открытии идентификатор сеанса читается.
Добавим, что этот сбор прост, когда иконка жертвы оказывается на странице, видимой атакующему (например, на странице отзывов товара, на который он сам оставил отзыв). URL иконки закреплён за каждым участником, поэтому, увидев его однажды, дальше можно брать этот URL напрямую. В любом случае действия атакующего — это только GET страницы или изображения как обычного посетителя, и с точки зрения сайта это не аномальный обмен данными.
Проследим весь поток от публикации до сбора
Загружает браузер жертвы, собирает атакующий. Проследите диаграмму, разделяя действия этих двух сторон.
sequenceDiagram
autonumber
participant AT as Атакующий
participant Q as Веб-приложение Q<br/>(example.jp)
participant V as Браузер жертвы
Note over AT,Q: Этап подготовки
AT->>Q: публикация отзыва в 15 частей<br/>(символы комментария пропускают HTML)
Note over Q: сохраняет опубликованную строку<br/>как есть<br/>(само сохранение здесь верно)
Note over V,Q: жертва открывает страницу V
V->>Q: GET страницы V
Note over Q: ★уязвимость здесь★<br/>сохранённый заголовок<br/>вставляется в HTML без экранирования
Q-->>V: HTML с атакующим скриптом
Note over V: выполняется как элемент script
V->>Q: GET /user/profile
Q-->>V: HTML с токеном
Note over V: читает document.cookie<br/>и делает его содержимым a.png
V->>Q: POST /user/upload<br/>(uploadfile=a.png, token=…)
Note over Q: сохраняет без проверки формата<br/>и публикует как иконку
Note over AT,Q: сбор
AT->>Q: GET иконки жертвы
Q-->>AT: файл с записанным идентификатором сеанса
Контроль исходящего трафика это не останавливает
Сложность этого пути становится очевидной, если нарисовать схему. Браузер жертвы от начала и до конца обменивается данными только с легальным сайтом (example.jp).
Таблица ниже сравнивает меры, следящие за подозрительными внешними адресатами, и меры, останавливающие выполнение скрипта. Из перечисленных здесь настроек работает только последняя. CSP — не условие задачи, а дополнение к практике.
| Мера | Против этой атаки | Причина |
|---|---|---|
| Фильтрация URL на прокси | Не работает | Единственный адресат обращения — легальный по роду деятельности интернет-магазин |
| Наблюдение за исходящим трафиком на межсетевом экране | Не работает | Обращений к незнакомому домену не возникает |
Ограничение connect-src в Content Security Policy (настройка вроде 'self', разрешающая тот же origin) |
Не работает | Оба обращения адресованы тому же origin и укладываются в разрешённую политикой область |
Ограничение script-src в Content Security Policy (конфигурация, не разрешающая 'unsafe-inline') |
Работает | Внедрённый встроенный скрипт вообще не выполняется |
Про вынос данных через XSS легко представить картину «cookie отправляются на сервер атакующего», но эта задача наглядно показывает, что местом выноса может стать сам сайт жертвы. Если внутри сайта есть место, куда можно писать и откуда можно читать, оно и станет местом передачи. Типичные примеры — функция загрузки файлов, открытое поле профиля, открытое поле заметок.
В CSP разделяйте «куда можно обращаться» и «что разрешено выполнять»
О CSP добавим: из настроек, часто применяемых на практике, надёжно останавливает эту атаку script-src, а не connect-src (ограничение адресатов). При конфигурации с script-src 'self' и без разрешения 'unsafe-inline' встроенный <script>, внедрённый в отзыв, вообще не выполняется. Если понимать CSP только как средство сузить обмен с внешним миром, эту разницу легко оценить неверно.8
connect-src тоже не бессилен в принципе. Настройка вроде connect-src 'none', запрещающая даже обращения к тому же origin, или список разрешённых адресатов, суженный до конкретных эндпоинтов, остановили бы и эти два XHR. Но сайтов, которые могут сузить настолько, немного, поэтому как реалистичная мера на первом месте оказывается script-src. Точное понимание таково: не «тот же origin вне области действия CSP», а «частая настройка, разрешающая тот же origin, его пропускает».
Пункт 3(3): что можно сделать с идентификатором сеанса
В пункте 3(3) спрашивают, что можно сделать с полученной информацией, не более чем в 40 символах.
Образец ответа: «Выдать себя за участника, обращавшегося к странице V, и воспользоваться функциями веб-приложения Q».3
По комментариям по оценке, доля правильных ответов на этот пункт была высокой. Там сказано, что влияние получения cookie атакующим на сайте интернет-магазина было хорошо понято.4
А если перечитать спецификацию из условия, в функции профиля участника явно есть страница регистрации данных банковской карты, доступная только участникам, вошедшим в систему. Что находится за выдачей себя за другого, условие задачи подготовило с самого начала.
8. Почему на сайте атакующего это не срабатывает — задание 4
Пункт 4 — самый принципиальный вопрос во всём этом задании.
Допустим, на сайте домена, который подготовил атакующий, разместили HTML с тем же скриптом, что на рисунке 4, и на этот сайт зашёл участник веб-приложения Q, вошедший в систему. И всё же из-за механизма веб-браузера атака не удаётся. Ответьте, что это за механизм, не более чем в 40 символах.
Образец ответа: «Механизм, при котором из скрипта на URL другого домена cookie не отправляются».3
В этой главе сначала проверяется, что произойдёт, если оставить скрипт из условия как есть, а затем разбираются условия CORS и SameSite. Важно не утверждать, как основание для ответа, настройки, которых в условии нет.
Основание ответа: к первому XHR cookie не прилагаются
Допустим, тот же скрипт размещён на evil.example и участник компании Q его открыл. Что произойдёт?
Сначала первое обращение уходит впустую. Если скрипт на evil.example отправляет XMLHttpRequest на https://example.jp/user/profile, это запрос с другого origin.
Решающее — то, что cookie не прилагаются. Скрипт с рисунка 4 не указывает withCredentials, поэтому к запросу с другого origin cookie не прикрепляются. С точки зрения сервера компании Q это обращение без входа в систему, и страница настроек профиля с токеном не возвращается. Скрипт не получает токен и останавливается на этом.9
Условное дополнение: CORS решает, можно ли прочитать ответ
Есть и вторая стена: ответ нельзя прочитать. Если компания Q не разрешила evil.example в Access-Control-Allow-Origin, браузер не проходит проверку CORS, считает это обращение сетевой ошибкой, onload не срабатывает, и до xhr.response добраться нельзя. Обычный сайт не возвращает этот заголовок незнакомому origin, так что на практике и здесь всё остановится.
Однако как компания Q настраивала CORS, в условии не написано. Даже если доступ с evil.example был разрешён, прочитать можно было бы ответ состояния без входа в систему, а токена в нём нет, значит, атака всё равно не удалась бы. Уже одного первого пункта — что cookie не прилагаются — достаточно, чтобы атака остановилась.
То, что cookie не прилагаются, — это то, что можно вывести из самого скрипта. Действует ли ещё и ограничение CORS, зависит от настроек компании Q. Не обращайтесь с обоими как с проверенными фактами.
Читать cookie и прикреплять их к запросу — разные вещи
Кроме того, и сами cookie прочитать нельзя. document.cookie возвращает cookie того origin, на котором выполняется скрипт (evil.example). Идентификатора сеанса, выданного для example.jp, там нет.
Образец ответа IPA берёт первое — «cookie не отправляются на URL другого домена».
И не запоминайте эти две вещи как один механизм. Это разные механизмы.
document.cookieне возвращает cookieexample.jpпотому, что cookie управляются раздельно по доменам. Между разными сайтами (разными регистрируемыми доменами), какevil.exampleиexample.jp, это разделение настройкой ослабить нельзя. Но между поддоменами одного домена — другое дело: с помощью атрибутаDomaincookie можно сделать общими дляsub.example.jpиexample.jp7- К XHR с другого origin cookie не прилагаются потому, что
withCredentialsпо умолчанию равенfalse. Здесь при совпадении условий всё меняется. Если скрипт укажетwithCredentials = true, атрибутSameSiteу cookie допускает межсайтовую отправку, и сервер вернётAccess-Control-Allow-Originс явно указанным origin отправителя вместе сAccess-Control-Allow-Credentials: true, то cookie будут отправлены и ответ можно будет прочитать
Дополнение к практике: что требуется при изменённых настройках
Здесь разделите причину фактического провала и что потребовалось бы, если переписать скрипт.
Из условия и рисунка 4 достоверно следует одно. withCredentials не указан, поэтому cookie не прилагаются, обращение считается неаутентифицированным, и токен не получен. Уже этого достаточно, чтобы атака остановилась.
Отсутствие разрешения CORS — тоже стена, мешающая прочитать ответ, но настройки компании Q в условии не написаны, поэтому считайте это условным подкреплением в духе «на обычном сайте так и настроено».
А что, если атакующий перепишет на withCredentials = true? Вот тогда и вступает в игру второе условие. Если SameSite у cookie не допускает межсайтовую отправку, то и при выставленном withCredentials эта cookie не будет приложена. Современные браузеры трактуют cookie без указанного SameSite как Lax, поэтому по умолчанию дело склоняется к стороне «не прилагается».
Как был настроен SameSite у cookie с идентификатором сеанса компании Q, в условии тоже не написано. Поэтому нельзя сказать, что сайт был защищён благодаря SameSite. Речь лишь о том, что SameSite может работать против атакующего, приходящего с учётными данными.
В сумме, чтобы прочитать аутентифицированный ответ с другого домена, должны совпасть три вещи: withCredentials = true, атрибут SameSite у cookie, допускающий межсайтовую отправку, и сервер, разрешающий origin отправителя через CORS с учётными данными. «Другой домен — значит всегда безопасно» неверно, но и «достаточно одного совпавшего условия, чтобы пробить» — тоже. Проверяя собственный сайт, не успокаивайтесь разделением cookie по доменам, а фактически проверьте настройки CORS (в частности, каким origin разрешены запросы с учётными данными) и атрибут SameSite у cookie.
Поэтому хранимый XSS так ценен
Этот пункт учит тому, почему атакующий настаивает на выполнении своего кода внутри сайта жертвы.
Даже подготовив на своём домене поддельный сайт с точным внешним сходством, браузер будет считать его другим сайтом. Через document.cookie cookie другого сайта не прочитать, а при XHR, отправляемом без учётных данных, как в скрипте этого пункта, для другого сайта вы даже не будете вошедшим в систему. Ценность для атакующего — в самом факте, что код выполняется на origin сайта жертвы. Хранимый XSS и есть средство этого добиться.
Не путайте «можно отправить» и «можно прочитать»
Но не читайте это как «с другого домена нельзя отправить вообще ни одного аутентифицированного запроса». Если SameSite у cookie настроен так, что допускает межсайтовую отправку (SameSite=None и т. п.), вредоносная страница может доставить запрос с cookie на сервер другой стороны. Типичный пример — POST формы; CORS управляет в основном тем, может ли скрипт прочитать ответ, а не тем, доходит ли запрос до сервера (правда, при добавлении пользовательского заголовка или отправке application/json предварительно идёт preflight, поэтому без разрешения сам запрос не отправляется). Именно поэтому атака CSRF возможна и поэтому нужен токен против CSRF.
Иначе говоря, скрипт с другого origin по умолчанию не может прочитать cookie другой стороны и прочитать ответ — но не отправить запрос.
Более того, эти два «не прочитать» различаются по силе. Разделение по доменам у document.cookie нельзя ослабить серверной настройкой, а вот можно ли прочитать ответ, зависит от настроек CORS на стороне другого сервера. Если сервер разрешает origin отправителя (для запросов с учётными данными — возвращая конкретный origin в Access-Control-Allow-Origin и вместе с ним Access-Control-Allow-Credentials: true), скрипт с другого origin тоже сможет прочитать ответ. В этом и причина проверять настройки CORS собственного сайта.
XSS особ потому, что перепрыгивает через все эти условия и с самого начала действует как origin сайта жертвы.
Иными словами, XSS — не «дефект, из-за которого на экране появляются странные символы», а дефект, ставящий атакующего в то же положение, что и легального пользователя вашего сайта. Именно это различие в понимании определяет приоритеты.
9. Сработавшие и несработавшие меры
Соберём всё до этого места в одну таблицу. Это самая важная часть статьи. Читайте её не как «есть ли мера», а как «какой этап этой атаки она останавливает».
Сначала сравним меры компании Q применительно к этой атаке
| Что у компании Q было / чего не было | Против этой атаки | Причина |
|---|---|---|
| Обмен по HTTPS | Не сработало | Это защита канала, не имеющая отношения к скрипту, внедрённому в страницу |
| Ограничение ввода: 50 символов для заголовка отзыва, 300 для текста | Не сработало | Публикация была разбита на 15 частей, а промежуточный HTML пропускался как комментарий JavaScript |
| Токен для загрузки (мера против CSRF) | Не сработало | Скрипт на том же origin может получить токен штатным порядком |
| Загрузка только после входа в систему | Не сработало | Работает браузер самой жертвы, вошедшей в систему |
| Экранирование при выводе (отсутствовало) | Сработает (устранение причины) | Опубликованная строка больше не интерпретируется как HTML, и скрипт вообще не выполняется |
| Атрибут HttpOnly у cookie (отсутствовал) | Сработает (компенсирующая мера) | Из document.cookie идентификатор сеанса больше не читается |
| Проверка формата и перекодирование загружаемых файлов (отсутствовали) | Сработает (компенсирующая мера) | Обычный текст нельзя сохранить как PNG, а строку, спрятанную в метаданных, стирает перекодирование. Но данные, закодированные пикселями изображения, остаются |
Первые четыре против этой атаки не сделали ничего. Если наложить последние три на деление из документа IPA «Как создать безопасный веб-сайт», первое — устранение причины, остальные два — компенсирующие меры5.
Одна оговорка: это не значит, что первые четыре — то, чего IPA мерами не признаёт. Деление IPA — устранение причины и компенсирующие меры, и проверка содержимого входных значений как раз отнесена к компенсирующим мерам. Но там есть оговорка «эта мера эффективна лишь ограниченно», и в этом случае мы как раз попали в это ограничение.
Останавливаемые этапы различаются — отсюда и эшелонированная защита
И важно, что любой одной из последних трёх хватило бы, чтобы цепочка атаки, выполненной в этой задаче, прервалась.
- Если бы было экранирование, скрипт вообще не выполнялся бы
- Если бы был HttpOnly, скрипт выполнился бы, но не смог прочитать cookie
- Если бы была проверка формата, cookie читались бы, но обычный текст нельзя было бы загрузить как PNG
Именно в таких ситуациях эшелонированная защита и имеет смысл. Но компенсирующие меры не заменяют устранение причины. Даже с HttpOnly, если XSS остался, атакующий может выполнять в браузере жертвы произвольные действия (покупку товаров, изменение регистрационных данных и т. п.). Атак, которым и красть идентификатор сеанса не нужно, можно придумать сколько угодно.
Почему в таблице не только проверка формата, но и перекодирование
То, что последняя строка таблицы написана в два этапа — «проверка формата и перекодирование», — имеет причину.
Различайте обычный текст этого случая и изображение правильного формата
Компания Q в условии вообще не проверяла формат файлов изображений, поэтому одна лишь проверка формата останавливает эту атаку. Однако это верно только для случая, когда атакующий присылает обычный текст. Атакующий может собрать корректный PNG-файл и спрятать строку в области метаданных или подобном месте. Тогда проверка «правильный ли формат» проходит.
Поэтому, чтобы сузить этот путь, нужно дойти до перекодирования загруженных изображений на стороне сервера и раздачи уже перекодированных (то есть не сохранять исходную последовательность байтов как есть).
Перекодирование тоже не универсально
Но и перекодирование не закрывает путь полностью. Если атакующий вписал идентификатор сеанса самими пикселями изображения и построил корректный PNG, обычное перекодирование сохранит эту видимую информацию, и атакующий сможет скачать и прочитать её. Перекодирование останавливает вариант с отправкой сырой последовательности байтов и вариант с сокрытием в области метаданных, но не информацию, закодированную как содержимое изображения.
Иначе говоря, пока внутри сайта есть «место, куда можно записать и откуда можно прочитать», полностью закрыть его как путь выноса нельзя. Это хорошо описывает природу компенсирующих мер. Компенсирующей мере недостаточно остановить наблюдаемую сейчас атаку — её нужно проектировать с учётом того, останется ли она работоспособной, когда атакующий немного изменит приём. И сколько бы их ни накладывать, они не заменяют экранирование при выводе — устранение причины.
Раздача с отдельного домена — мера против другой угрозы
Отметим, что проектирование с раздачей загруженных файлов с домена, отдельного от основного, тоже часто рекомендуют, но это не мера против этого пути выноса. Атакующий может скачать изображение и с отдельного домена, а политика same-origin — не механизм сохранения в тайне опубликованных последовательностей байтов. Раздача с отдельного домена работает против угрозы того, что загруженный файл сам выполнится как активное содержимое на origin сайта жертвы (случай загрузки HTML или SVG). Понимайте это отдельно, как меру против другой угрозы.
Как «судить по содержимому» — зависит от формата файла
Сам подход — судить о файле не по заявлению отправителя (расширению или Content-Type), а по содержимому — базовое действие, нужное не только в вебе. Но то, что значит «судить по содержимому», зависит от формата. Для формата вроде PNG, у которого в начале фиксированная последовательность байтов, её можно проверить. У CSV, напротив, стандартизованной сигнатуры нет, а BOM в начале говорит лишь о кодировке символов и не доказывает, что содержимое — CSV. Как разобрано в статье CSV — не «просто текст». Практика CSV в бизнес-приложениях на C# (кодировки, совместимость с Excel, защита от инъекций), для CSV проверяют, разбирается ли он в ожидаемом диалекте и укладывается ли в ожидаемую схему и предел числа записей.
10. Точки проверки в собственном коде
Если свести это задание к чек-листу код-ревью, получится следующее. Для веб-приложения с функциями участников и публикаций его можно применять как есть.
Пункты 1–3 — про ввод и вывод, 4 — про cookie, 5–6 — про загрузку, 7–8 — про CSP и токен. В первой половине проверяется реализация, убирающая коренную причину, во второй — область действия компенсирующих мер.
- Действует ли экранирование при выводе во всех местах вывода? Выпишите все места, где автоматическое экранирование шаблонизатора явно отключено (
raw,| safe,dangerouslySetInnerHTMLи т. п.), и проверьте, можете ли для каждого объяснить, почему отключение допустимо - Не считаете ли вы ограничения на стороне ввода мерами против XSS? Ограничения на число символов и на допустимые символы — требования спецификации, а не устранение причины XSS
- Не экранируете ли вы при вводе? Роль ввода — проверка, отсеивающая значения вне спецификации, а не экранирование. В момент ввода приёмник (HTML, CSV, JSON, почта, логи) ещё не определён, поэтому экранирование при вводе ведёт к двойному экранированию и порче данных. Со сборкой SQL-запросов так же: устранение причины там — плейсхолдеры
- Установлены ли на cookie
HttpOnly,SecureиSameSite? Особенно проверьте cookie с идентификатором сеанса - Проверяете ли вы формат загруженного файла по содержимому, а не по расширению или Content-Type? Информация, заявленная отправителем, материалом для проверки не является. Но одна проверка формата пропускает приём с сокрытием данных в области метаданных корректного изображения
- Перекодируете ли вы загруженные изображения перед раздачей? Не возвращаете ли исходную последовательность байтов как есть? Учитывайте, однако, что даже перекодирование оставляет нетронутой информацию, закодированную пикселями, поэтому «место, куда можно записать и откуда можно прочитать» закрыть полностью нельзя. Отметьте и то, что раздача с отдельного домена — мера против угрозы выполнения загруженного файла на origin вашего сайта, а не против этого пути выноса
- Способна ли ваша конфигурация остановить встроенные скрипты средствами CSP? Критерий — не «написано ли там
'unsafe-inline'», а «разрешены ли встроенные скрипты фактически». Если вscript-srcесть nonce или hash, браузеры начиная с CSP Level 2 игнорируют'unsafe-inline', поэтому при обратно совместимой политике, где рядом с nonce указан и'unsafe-inline', внедрённый скрипт без nonce блокируется8 - Можете ли вы объяснить, что именно защищает токен? Токен против CSRF не защищает от XSS. Каждому нужна своя отдельная мера
Особенно первый пункт — типичная картина, которую находят при реальных разборах. Нередко выясняется, что команда считала себя в безопасности, потому что фреймворк экранирует автоматически, а автоматическое экранирование оказалось отключено ровно в одном месте ради отдельного требования «вставить HTML как есть».
В заключение — какая способность действительно проверяется этой задачей
Список разобранных мер сведён в «Сначала выводы» в начале. В завершение скажем одно о самом задании.
Как экзаменационное задание оно хорошо тем, что спрашивает не «знаете ли вы, что такое XSS», а «умеете ли вы различить, какие из имеющихся у вас мер действительно работают».
Компания Q не бездействовала. Она обменивалась данными по HTTPS, ставила ограничения на число символов во вводе, требовала токен для загрузки, ограничивала функции вошедшими в систему участниками. В перечислении это выглядит как вполне защищённая разработка. И всё же всё было обойдено. Наоборот, три отсутствовавшие вещи — все неброские, не из тех, что попадают в примечания к релизу.
Разговоры о безопасности сложны потому, что число мер не пропорционально силе защиты. Вопрос в том, можете ли вы по каждой мере сказать, не «что мы делаем», а «какой этап какой атаки она останавливает». Это способность, нужная не только для квалификационного экзамена: это важнейшее умение и для тех, кто выслушивает объяснения о мерах безопасности, и для тех, кто их реализует.
Смежные области консультаций
KomuraSoft LLC занимается созданием сайтов с функциями участников и публикаций, а также проверкой проектирования — не закралась ли та же дыра в существующее веб-приложение.
- Создание и обновление сайтов
- Техническая консультация и ревью проектирования
- Расследование сбоев и анализ причин
- Связаться с нами
Источники
-
IPA (Information-technology Promotion Agency, Japan), Буклеты с заданиями, распределение баллов, образцы ответов и комментарии по оценке (2023 год, 5-й год Рэйва), в том числе «5-й год Рэйва, осенняя сессия, экзамен специалиста по обеспечению информационной безопасности, вторая половина дня, задания». О функциях веб-приложения Q компании Q (регистрация участников, вход в систему и выдача идентификатора сеанса в виде cookie, ограничение ввода в функции отзывов на товары — 50 символов для заголовка отзыва и 300 для текста, загрузка иконки и регистрация данных банковской карты в функции профиля участника, приём загрузкой иконки файла изображения и токена как параметров, отображение загруженной иконки на странице настроек профиля участника и на страницах отзывов), о случае, когда на странице V из 16 отзывов показывались только 2, о том, что HTML страницы V содержал 15 публикаций участника A и длинный скрипт, о содержимом извлечённого скрипта, о наличии в веб-приложении Q уязвимости, из-за которой выполнялся введённый участником скрипт, о том, что на cookie не был установлен атрибут HttpOnly, и о том, что формат загружаемых файлов изображений не проверялся. Формулировки заданий 1–4 также взяты из этого буклета. ↩ ↩2
-
IPA (Information-technology Promotion Agency, Japan), Часто задаваемые вопросы об экзаменах. О том, что для использования публикуемых организацией прошлых экзаменационных заданий разрешение и плата не требуются, кроме случаев, специально оговорённых законом; что при этом авторские права не отчуждаются; что источник нужно указывать в форме «год, сессия, категория экзамена, временной интервал, номер задания и т. д.» (пример, который приводится, — «Источник: 31-й год Хэйсэй, весенняя сессия, экзамен на базового специалиста по информационным технологиям, утро, задание 1»); и что при частичном изменении задания это также нужно указать. ↩
-
IPA (Information-technology Promotion Agency, Japan), 5-й год Рэйва, осенняя сессия, экзамен специалиста по обеспечению информационной безопасности, образцы ответов. О цели задания 1 (на теме реагирования на инцидент, вызванный использованием уязвимости в программе веб-приложения, проверяется способность прочитать по HTML и ECMAScript использованную уязвимость и связанные с ней проблемы и разработать меры) и об образце ответа для каждого пункта (пункт 1(1) — «(b) хранимый XSS»; пункт 1(2) — «Применять экранирование к заголовку отзыва перед выводом.»; пункт 2 — «Публикацию провели в несколько приёмов так, что HTML закомментировался и превратился в один скрипт.»; пункт 3(1) — «Загружает идентификатор сеанса как иконку, вместе с токеном, полученным из ответа XHR.»; пункт 3(2) — «Скачать иконку участника и извлечь из неё строку идентификатора сеанса.»; пункт 3(3) — «Выдать себя за участника, обращавшегося к странице V, и воспользоваться функциями веб-приложения Q.»; пункт 4 — «Механизм, при котором из скрипта на URL другого домена cookie не отправляются»). ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
IPA (Information-technology Promotion Agency, Japan), 5-й год Рэйва, осенняя сессия, экзамен специалиста по обеспечению информационной безопасности, комментарии по оценке. О том, что доля правильных ответов на задание 1 в целом была средней; о пункте 1(1) — что встречались экзаменуемые, ошибочно отвечавшие «DOM Based XSS», возможно, потому что скрипт использовал DOM, вместе с указанием, что уязвимости следует понимать точно, вплоть до их особенностей и способов защиты; о пункте 2 — что встречались ответы, в которых проверка представляется недостаточной, например «атакующий удалил ограничение на ввод инструментами разработчика и опубликовал», вместе с указанием, что следует развивать способность внимательно проверять оставленные атакующим следы и точно понимать метод атаки; а также о том, что доля правильных ответов на пункт 3(3) была высокой и что влияние получения cookie атакующим на сайте интернет-магазина было хорошо понято. ↩ ↩2 ↩3 ↩4 ↩5
-
IPA (Information-technology Promotion Agency, Japan), Как создать безопасный веб-сайт. О том, что для 11 видов уязвимостей веб-сайтов угрозы и меры показаны раздельно как «устранение причины» (реализация, убирающая саму причину уязвимости) и «компенсирующие меры» (меры, снижающие вероятность успеха атаки или ущерб, если уязвимость осталась); что среди мер по устранению причины межсайтового скриптинга названо применение экранирования ко всем элементам, выводимым на веб-страницу; а также о прилагаемом чек-листе реализации защиты и отдельных приложениях «Как безопасно вызывать SQL» и «Спецификация диагностики веб-сайта». ↩ ↩2 ↩3
-
IPA (Information-technology Promotion Agency, Japan), Как создать безопасный веб-сайт — 1.1 SQL-инъекция. О том, что как устранение причины SQL-инъекции названо «реализовать всю сборку SQL-запросов через плейсхолдеры» и что статические плейсхолдеры (prepared statement) считаются формой, в которой уязвимость в принципе возникнуть не может; что как реализация для случая сборки SQL-запроса конкатенацией строк указано «если сборка SQL-запроса выполняется конкатенацией строк, используйте API СУБД, выполняющий экранирование, чтобы правильно составить литералы SQL-запроса», то есть экранирование применяется к генерации литералов, составляющих SQL-запрос, а не в момент получения ввода; и что наряду с этим как устранение причины названо «не указывать SQL-запросы напрямую в параметрах, передаваемых веб-приложению», а как компенсирующие меры — «не выводить сообщения об ошибках в браузер как есть» и «давать учётной записи базы данных подходящие права». ↩
-
IETF, RFC 6265: HTTP State Management Mechanism, Section 4.1.2.6 “The HttpOnly Attribute”. О том, что атрибут HttpOnly ограничивает область действия cookie HTTP-запросами и в частности предписывает пользовательскому агенту опускать cookie при предоставлении доступа к cookie через «не-HTTP» API, например через API веб-браузера, открывающее cookie скриптам. А также о том, что атрибут Secure (Section 4.1.2.5) ограничивает отправку cookie только защищённым каналом. ↩ ↩2
-
W3C, Content Security Policy Level 3. Об управлении выполнением встроенных скриптов через script-src, об управлении адресатами через connect-src и об обращении с unsafe-inline в списке источников, содержащем nonce или hash. Это основание для практических замечаний в главах 7 и 10, а не документ, показывающий настройки компании Q. ↩ ↩2
-
WHATWG, XMLHttpRequest Standard — The withCredentials getter and setter. О том, что он управляет включением учётных данных в запрос с другого origin и что его начальное значение — false. В главе 8 он цитируется как основание читать отсутствие указания withCredentials в скрипте из условия как то, что cookie не прилагаются. ↩
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Разбор вопроса 2 дневного сеанса экзамена специалиста по обеспечению информационной безопасности, осень 5-го года Рэйва — файлы уносят через гостевой Wi-Fi
На материале вопроса 2 дневного сеанса экзамена специалиста по обеспечению информационной безопасности осенью 5-го года Рэйва (2023) стат...
Заказчику сайта тоже стоит знать: как пользоваться документом IPA «Как создать безопасный веб-сайт»
По какому критерию проверять безопасность корпоративного сайта? Разбираем 11 уязвимостей и меры из документа IPA «Как создать безопасный ...
10 главных угроз информационной безопасности 2026: как читать рейтинг и что защищать МСБ
В рейтинге IPA «10 главных угроз информационной безопасности 2026» атаки программ-вымогателей заняли 1-е место 11-й год подряд, атаки на ...
С чего малому и среднему бизнесу начать информационную безопасность — редакция 4.0 руководства IPA
С чего малому и среднему бизнесу начинать защиту информации. По редакции 4.0 руководства IPA разбираем шесть базовых правил, самооценку з...
Экзамен SC весны 2024, задание 1 части PM: JWT alg=none, авторизация API и временные меры WAF
На материале задания 1 части PM экзамена IPA SC весны 2024 разбираем alg=none у JWT, авторизацию API, Mass Assignment, перебор четырёхзна...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка веб-сайтов
На сайтах с функциями участников и публикации записей вопросы этой статьи — экранирование при выводе и настройка атрибутов cookie — напрямую влияют на качество разработки.
Технические консультации и ревью дизайна
Выявление с точки зрения ревью дизайна того, где в существующем веб-приложении такая же дыра, — это работа в рамках технических консультаций.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Почему XSS в этой задаче «хранимый»? Ведь скрипт работает с DOM — разве это не DOM Based XSS?
- Тип уязвимости определяется тем, «где» строка атакующего становится исполняемым кодом, то есть тем, находится ли уязвимое место вывода (sink) на стороне сервера или на стороне браузера. В этой задаче сервер вставлял опубликованный атакующим заголовок отзыва в HTML как есть и возвращал его. Sink находится в серверной обработке вывода, а строка сохраняется и затем раздаётся всем, кто с этого момента открывает страницу, поэтому это хранимый XSS. DOM Based XSS обозначает категорию, в которой JavaScript в браузере делает значение исполняемым, например через присваивание в innerHTML. Использует ли внедрённый скрипт API DOM вроде XMLHttpRequest или getElementById, к типу уязвимости отношения не имеет. Кроме того, даже если сервер возвращает сохранённое значение как безобидный текст или JSON, оно станет DOM Based XSS, если JavaScript на стороне браузера передаст его в innerHTML. Судить нужно не по тому, видна ли строка в ответе, а по тому, где она стала исполняемой. В комментариях IPA по оценке тоже отмечено, что встречались экзаменуемые, ошибочно отвечавшие «DOM Based XSS», возможно, потому что скрипт использовал DOM.
- В заголовке отзыва было ограничение в 50 символов. Почему всё же удалось выполнить длинный скрипт?
- Атакующий разделил нагрузку на части, укладывающиеся в одну публикацию, и опубликовал её несколько раз. В первой публикации тег script открывается в середине заголовка, а в конце заголовка открывается блочный комментарий JavaScript (косая черта и звёздочка). Во второй публикации этот комментарий закрывается в самом начале, затем пишется следующая инструкция, а в конце снова открывается комментарий — и такой приём повторяется. В результате весь HTML, оказавшийся между публикациями (теги div и т. п.), попадает внутрь комментария JavaScript и игнорируется, а разделённые фрагменты соединяются и выполняются как один скрипт. Ограничение на число символов во вводе ограничивало только длину одной публикации и не ограничивало число публикаций.
- Куда был отправлен украденный идентификатор сеанса?
- На внешний сервер он не отправлялся. Скрипт взял содержимое cookie как содержимое файла, дал файлу имя «a.png», задал MIME-тип «image/png» и отправил его в собственную функцию загрузки иконок участников того самого интернет-магазина, которым пользуется жертва. Загруженная иконка показывается на странице отзывов и в других местах, поэтому атакующий может просто скачать это изображение и извлечь идентификатор сеанса из строки внутри него. Браузер жертвы общался только с легальным сайтом, подозрительного внешнего трафика не возникает, поэтому это путь, который не заметить ни фильтрацией URL на прокси, ни наблюдением за исходящим трафиком на межсетевом экране.
- Для загрузки был нужен токен. Почему атака всё же удалась?
- Потому что этот токен — механизм для отсечения самовольных запросов с другого сайта (мера против CSRF), а не мера, рассчитанная на скрипт, работающий на том же сайте. Атакующий скрипт сначала обращается к странице настроек профиля через XMLHttpRequest, получает токен тем же порядком, что и штатный экран, а затем выполняет загрузку, приложив этот токен. Из-за XSS код атакующего работает на том же origin, что и у жертвы, поэтому ему доступно всё, что доступно легальному пользователю. Токен против CSRF не защищает от XSS.
- Говорят, что при SQL-инъекции важно экранирование ввода. Экранирование при выводе — только для XSS?
- Нет, и при SQL-инъекции «экранирование при вводе» мерой не является. Как корневое решение IPA называет «реализовать всю сборку SQL-запросов через плейсхолдеры». Экранирование упомянуто как альтернатива для случая, когда SQL-запрос приходится собирать конкатенацией строк, но и там оно служит тому, чтобы «правильно составить литералы SQL-запроса», то есть выполняется в момент сборки SQL-запроса, а не в момент получения ввода. Общий принцип таков: экранирование выполняется там, где решается, в мир какой грамматики уходят данные. Если данные уходят как HTML — экранирование HTML; если становятся частью SQL-запроса — плейсхолдеры; если становятся частью команды оболочки — правила оболочки. Причина, по которой нельзя экранировать при вводе, в том, что на этот момент место вывода ещё не определено. Одни и те же данные уходят и на HTML-страницу, и в CSV, и в JSON, и в письмо-уведомление, и в лог, поэтому экранирование HTML при вводе приводит к тому, что в CSV попадают сущности как есть, а содержимое базы данных перестаёт совпадать с исходным вводом, и поиск с агрегацией начинают давать неверные результаты. Это не значит, что на стороне ввода не нужно делать ничего. Роль стороны ввода — не экранирование, а проверка (валидация): отсеивать значения, невозможные по спецификации.
- Какие меры стоит взять из этой задачи в практику?
- Корневое решение — применять экранирование ко всем пользовательским данным, включая заголовок отзыва, непосредственно перед выводом в HTML. В дополнение к этому как страховочные меры назовём: установить атрибут HttpOnly на cookie с идентификатором сеанса, чтобы скрипт не мог его прочитать; перекодировать загруженные изображения на стороне сервера, чтобы исходная последовательность байт не сохранялась; настроить Content Security Policy так, чтобы можно было остановить выполнение встроенных скриптов. Если бы компания Q из этой задачи реализовала хотя бы одну из этих мер, цепочка атаки прервалась бы где-то по пути. Правда, страховочные меры можно обойти, если атакующий сменит приём, поэтому они не заменяют экранирование — корневое решение.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.