Перейти к содержимому

Динамический краулер для Spa: как исследовать состояния веб-приложения

Сколько страниц у SPA: как мы реализовали динамический краулер

Меня зовут Владимир Гринчуков. В Positive Technologies мы вместе с командой PT BlackBox разрабатываем сканер безопасности. На вход он получает адрес веб-приложения, а на выходе формирует перечень найденных уязвимостей.

Как и любой инструмент анализа методом "чёрного ящика", сканер начинает работу со сбора поверхности атаки. Ему необходимо обнаружить эндпойнты приложения, HTTP-методы и параметры запросов, после чего проверить их на наличие уязвимостей. Компонент, отвечающий за обход приложения и поиск входных точек, называется краулером.

На первый взгляд задача выглядит элементарно: открыть страницу, найти ссылки и формы, перейти по найденным адресам, а затем повторить процесс. Однако для полноценного DAST-инструмента этого недостаточно. Краулер должен работать автономно, завершать обход за разумное время, давать воспроизводимый результат и не тратить ресурсы на бесконечное повторение одних и тех же действий.

Почему обычного краулера недостаточно

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

Но современные приложения часто строятся на React, Vue и других JavaScript-фреймворках. Это одностраничные приложения, или SPA. После первоначальной загрузки браузер получает минимальный HTML, а дальнейшее содержимое формируется скриптами. Пользователь может открывать меню, переключать вкладки и отправлять формы, при этом URL останется прежним, а данные будут передаваться в фоне через AJAX или fetch.

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

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

Что считать динамическим приложением

Будем считать приложение статическим, если все его эндпойнты можно получить без взаимодействия с интерфейсом. Иными словами, достаточно открыть страницу, отрендерить её и рекурсивно извлечь ссылки и формы из DOM.

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

В таком приложении URL часто описывает лишь одно исходное состояние. Например, при открытии личного кабинета меню закрыто, вкладки скрыты, а нужные кнопки ещё не созданы. После каждого взаимодействия интерфейс меняется и может порождать новые элементы. Поэтому краулер должен исследовать не только адреса, но и состояния браузера.

Фактически перед ним стоят два ключевых вопроса:

1. С какими объектами на странице можно взаимодействовать?
2. Как изменилось приложение после конкретного действия?

Получить ответы только из HTML невозможно. Элемент может выглядеть как обычный контейнер, но быть интерактивным. Обработчик события способен добавляться через `addEventListener`, храниться во внутренней системе фреймворка или находиться в закрытом Shadow DOM. React, например, часто использует делегирование событий и регистрирует обработчики на корневом контейнере, а не непосредственно на кнопках.

Возможна и обратная ситуация: визуально элемент похож на кнопку, но фактически не реагирует на нажатие. Поэтому одного анализа тегов, классов и атрибутов недостаточно.

Откуда брать список действий

Первый источник - DOM после отрисовки. Из него можно извлечь кнопки, ссылки, поля ввода, чекбоксы, списки и другие элементы управления. Дополнительно учитываются атрибуты `role`, `tabindex`, обработчики, доступность элемента, его размеры и положение на экране.

Второй источник - поведение браузера. Краулер может выполнять пробные действия и фиксировать результат: изменился ли DOM, появился ли новый запрос, открылась ли панель, поменялся ли URL, возник ли диалог или сообщение об ошибке.

Третий источник - сетевой трафик. Даже если визуально ничего не изменилось, клик может отправить запрос. Для сканера это уже значимый результат: найден новый эндпойнт, его метод, параметры и фактическое состояние приложения, в котором запрос был выполнен.

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

Главная трудность - комбинаторный взрыв

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

Полный перебор практически невозможен. Он приводит к долгому сканированию, повторным запросам и большому числу почти одинаковых состояний. Кроме того, некоторые действия могут менять данные, создавать объекты, отправлять письма или запускать другие нежелательные процессы.

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

Как определить, что состояние уже встречалось

Сравнивать страницы по исходному URL нельзя: в SPA он может не меняться вообще. Недостаточно и простого сравнения HTML, поскольку фреймворк способен менять служебные атрибуты, порядок узлов или случайные идентификаторы.

Мы рассматриваем состояние как совокупность нескольких признаков:

- структура и содержимое DOM;
- видимые и доступные пользователю элементы;
- URL и состояние истории браузера;
- активные формы и значения полей;
- произошедшие сетевые запросы;
- изменения интерфейса после действия.

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

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

Произвольное блуждание и граф состояний

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

Более удобная модель - представить приложение в виде графа. Вершины графа соответствуют состояниям браузера, а рёбра - действиям пользователя. Клик, ввод текста, прокрутка или отправка формы переводят приложение из одного состояния в другое.

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

На практике применяется гибридная стратегия. Сначала обрабатываются очевидные элементы: ссылки, кнопки, формы и поля ввода. Затем добавляются менее явные кандидаты, обнаруженные по геометрии, ARIA-атрибутам, событиям и реакции интерфейса. Для каждой операции устанавливается глубина, стоимость и уровень риска.

Работа с вводом и повторяемостью

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

Важно разделять исследование интерфейса и проверку уязвимостей. На этапе обхода не следует без необходимости отправлять разрушительные операции. Для потенциально опасных действий применяются ограничения, специальные маркеры, пропуск или выполнение только в разрешённом режиме.

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

Что получилось в итоге

Динамический краулер для SPA - это не просто парсер HTML с поддержкой JavaScript. Он должен наблюдать за браузером, распознавать возможные действия, выполнять их, отслеживать изменения интерфейса и связывать эти изменения с сетевыми запросами.

Главный объект исследования здесь - не набор страниц, а граф состояний приложения. Один и тот же URL способен соответствовать десяткам разных экранов и сценариев, а важный эндпойнт может появиться только после длинной последовательности действий.

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

Именно такой подход позволяет сканеру находить точки входа, которые недоступны статическому обходу: скрытые формы, фоновые API-запросы, динамические меню, модальные окна и операции, возникающие только после взаимодействия пользователя с приложением.

Прокрутить вверх