Динамический рендеринг — это техника, которая используется, чтобы отдавать поисковикам и ботам заранее отрендеренные веб‑страницы. В этой статье я исследую, как две популярные утилиты для динамического рендеринга добавляют уязвимости в веб‑приложение, если настроены неправильно, и попробую объяснить, как я использовал уязвимость в одном из них, чтобы захватить сервер компании в рамках bug bounty.
Для создания сайтов и веб‑приложений активно применяются фреймворки JavaScript — вместо статичных страниц HTML теперь популярно делать PWA (progressive web apps) и SPA (single page applications), которые формируют большую часть контента в браузере пользователя. У этого подхода масса преимуществ, и на вебе это позволяет сделать отзывчивый интерфейс, но в то же время такой подход недружелюбен для SEO, потому что большинство поисковиков и ботов не понимают JavaScript и не могут рендерить страницы самостоятельно.
Один из распространенных способов помочь ботам в таком случае — это открыть запрашиваемую страницу в headless-браузере на стороне сервера, дождаться, пока страница отрисуется, и вернуть получившийся HTML, предварительно почистив его от лишних тегов. Этот метод и называется «динамический рендеринг» и сейчас активно продвигается компанией Google как возможность оптимизировать сайт для поиска.
Я наткнулся на этот тип приложений, когда проводил немного другое исследование: я искал уязвимости в модулях npm, которые используют headless-браузеры. Я написал правила для Semgrep (утилиты с открытыми исходниками, в разработке которой я принимаю участие) и применил их к тысячам модулей, которые используют Puppeteer, Playwright и PhantomJS в качестве зависимостей. Находок было много, и после расследования и разбора результатов я обнаружил множество модулей, помогающих веб‑мастерам в организации динамического рендеринга.
Популярность динамического рендеринга растет, поэтому будет небесполезно понять, что может пойти не так в продакшене при его использовании.
В своем исследовании я разобрал два самых популярных приложения для динамического рендеринга — Rendertron и Prerender, но описанные атаки можно использовать и для других приложений такого типа.
Также я немного расскажу о том, как мне удалось применить полученные знания при поиске уязвимостей в рамках bug bounty.
Архитектура
Один из возможных способов показать поисковому боту подходящий для индексации контент работает так: перехватывается запрос, страница рендерится на сервере, а результат в виде HTML со всем нужным содержимым возвращается боту.

- Сервер определяет, что запрос приходит от краулера, по заголовку User-Agent (в некоторых случаях — по параметрам URL).
- Запрос перенаправляется приложению для динамического рендеринга.
- Приложение для динамического рендеринга запускает headless-браузер и открывает исходный URL так, будто его смотрит обычный пользователь.
- Получившийся HTML очищается от уже не нужных тегов <<wbr />script> и возвращается на сервер.
- Сервер возвращает результат краулеру.
Разведка
На каких страницах обычно используется динамический рендеринг? Эти страницы, скорее всего, будут в открытом доступе, поскольку цель динамического рендеринга — улучшить их индексируемость. Контент на этих страницах будет создаваться при помощи JavaScript, при этом данные на странице меняются динамически. Например, это может быть новостной сайт, который постоянно обновляется, или часто обновляемый список популярных продуктов в интернет‑магазине.
Когда потенциальная цель найдена, можно проверить, использует ли она динамический рендеринг, отправив несколько запросов с разными значениями заголовка User-Agent.
Вот запрос, который притворяется Google Chrome:
А вот запрос якобы от бота Slack:
Если ответы от сервера различаются, а ответ на запрос от поддельного краулера приходит в виде красивого HTML без тегов <<wbr />script>, это означает, что сайт использует динамический рендеринг.
www
В качестве подопытного я использовал демосайт Google для фреймворка Polymer. Под капотом у него Rendertron.

Подробности того, на какие конкретно значения User-Agent реагирует приложение, можно посмотреть в исходном коде Rendertron (файл middleware.ts). Также Rendertron всегда возвращает заголовок X-Renderer: <wbr />Rendertron. Prerender может писать в ответах X-Prerender: <wbr />1, но это не умолчательное поведение.
Оба фреймворка дают разработчикам возможность управлять заголовками ответа с помощью метатегов на странице. Это полезно для детекта динамического рендеринга.
Пример для Prerender:
Пример для Rendertron:
SSRF по-легкому
Легче всего захватить приложение для динамического рендеринга, если оно доступно извне. Тогда можно взаимодействовать с ним напрямую и отправлять через него произвольные запросы, включая запросы к локальной инфраструктуре.
Существуют некоторые запреты на доступ к локальным адресам, но в зависимости от версии приложения их можно попробовать обойти.
Rendertron
Rendertron проще всего найти, потому что у него есть интерфейс, который позволяет отправлять запросы и делать скриншоты.

- Версия 3.1.0 — есть возможность задать список разрешенных URL (но их нужно настроить самому).
- Версия 3.0.0 — есть блокировка прямых запросов к Google Cloud, тем не менее ее можно обойти, отправив запросы через iFrame, блокировка не распространяется на другие облачные платформы (AWS, Digital Ocean и прочие).
- Старые версии блокируют запросы к Google Cloud, но разрешают запросы к бета‑версии API ( http://<wbr />metadata.<wbr />google.<wbr />internal/<wbr />computeMetadata/<wbr />v1beta1/).
- Версия 1.1.1 и младше — разрешены любые запросы.
Rendertron API (из документации):
- GET /<wbr />render/:<wbr />url — отобразит и сериализует страницу.
- GET /<wbr />screenshot/:<wbr />url и POST /<wbr />screenshot/:<wbr />url — делает скриншот страницы.
Дополнительные настройки для headless-браузера можно передавать через объект JSON POST-запросом. Подробности смотри в документации Puppeteer. Еще можно указать тип (по умолчанию JPEG) и кодировку (по умолчанию двоичная).
Итак, если ты наткнулся на рабочий Rendertron, первое, что можно сделать, — это предпринять SSRF-атаку и, к примеру, получить токены от облака следующим способом:
Либо:
Если запросы блокируются, все еще есть шанс заставить Headless Chrome открыть iFrame и показать скриншот, содержащий метаданные: отправить запрос на /<wbr />screenshot и направить Rendertron на страницу, которую ты контролируешь.
HTML по адресу www.<wbr />attackers-website.<wbr />here содержит iFrame, который обращается к API Google Cloud, доступному только на сервере.
В результате мы получаем скриншот с фреймом, который содержит секретный токен.

Этот баг исправлен в версии 3.1.0.
Prerender
У Prerender нет фронтенда, поэтому его сложнее обнаружить. Поиск осложняется еще и тем, что запрос на / возвращает статус 400 без интересных заголовков:
API Prerender выглядит следующим образом.
- GET /:<wbr />url
- GET /<wbr />render?url=:<wbr />url
- POST /<wbr />render?url=:<wbr />url
Список всех настроек можно найти в документации, основные выводы:
- Prerender тоже может делать скриншоты;
- followRedirects (по умолчанию false) разрешает перенаправления с одного адреса на другой.
Единственный способ определить использование Prerender — это отправить запрос по адресу /<wbr />render?url=http://<wbr />www.<wbr />example.<wbr />com и проверить результат. У Prerender нет встроенной блокировки запросов к облачным API, но он позволяет пользователям задавать списки разрешенных и заблокированных URL, поэтому в зависимости от настроек есть вероятность, что может получиться запрос вроде такого:
Также Prerender соединяется с Chrome через отладочный интерфейс, который всегда открыт на порте 9222, поэтому если запросы на этот адрес разрешены, то есть возможность вытащить Chrome ID.
Теперь можно слать запросы WebSocket к Chrome напрямую и таким образом управлять встроенным браузером. Например, открывать новые вкладки, отправлять произвольные запросы, читать локальные файлы (подробности — в документации Chrome DevTools Protocol).
В статье я фокусируюсь на том, чтобы попытаться вытащить секреты через API облачных провайдеров, но важно помнить, что, если в Rendertron или Prerender запросы к облаку запрещены, все еще можно попытаться отправить запросы к другим частям инфраструктуры — например, к кешу или базам данных.
Во время исследования я нашел несколько серверов c торчащим наружу Rendertron, но bug bounty на них не было, поэтому трогать их я не стал.
Атаки через веб-приложения
В поисках подходящих серверов (с уязвимостью и подлежащих bug bounty) я просто отправлял на все возможные домены запросы с заголовком User-Agent: <wbr />Slackbot <wbr />blabla. Всего один раз я получил в ответ с заголовком X-Renderer: <wbr />Rendertron, но этого оказалось достаточно, чтобы заработать вознаграждение.
Если приложение для динамического рендеринга не торчит наружу, но ты смог определить, что сайт его использует, все еще есть шанс провести атаку. В этом может помочь любой способ встроить свой контент в страницу или перенаправить страницу на ту, в которой можно контролировать содержимое. Простейший вариант — это найти open redirect. Который, к слову, многие программы bug bounty не принимают за уязвимость.
Если open redirect найден, то легко устроить атаку, просто отправив запросы:
Также можно перенаправить на страницу, содержащую фреймы, если прямые запросы заблокированы.

Поиск XSS- или HTML-инъекции выглядел сложной задачей, поэтому я сфокусировался на поиске open redirect. Мне повезло, и цель, которую я обнаружил, была уязвима к нему.
Большинство шпаргалок и обучающих материалов по open redirect фокусируются на перенаправлениях, которые происходят через сервер. Но так как динамический рендеринг используется на страницах с большим количеством сложного JavaScript, больше вероятность найти уязвимость на стороне клиента.
С этой задачей мне сильно помог Semgrep. Я набросал кучу шаблонов того, как может возникнуть редирект в JavaScript, и сканировал весь код на страницах, принадлежащих моей цели. Буквально в течение часа open redirect был найден.
Теперь осталось только заставить headless-браузер совершить перенаправление и вытащить метаданные от Google Cloud (URL был изменен, чтобы не разглашать информацию о приватной bug bounty).

Это сработало, и я получил вознаграждение за найденную уязвимость.
Мне повезло — я наткнулся на устаревшую версию, которая не блокировала прямые запросы к метаданным, но, если бы они были заблокированы, все равно была бы возможность запросить их через iFrame. Однако есть одна проблема — получить содержимое этого iFrame. Для этого можно использовать функцию снятия скриншотов страницы. Сценарий атаки выглядит следующим образом.

1. Страница, которая контролируется атакующим, открывается во вкладке headless-браузера — «Страница #1».
2. Это заставляет браузер отправить запрос самому себе (локально) и показать результат рендеринга с веб‑страницей атакующего («Страница #2»).
3. Headless-браузер открывает URL («Страница #3»), который снова отправляет запрос в приложение для рендеринга.
4–5. Браузер открывает еще одну страницу, контролируемую атакующим, — http://<wbr />www.<wbr />attackers-website.<wbr />url/<wbr />exploit.<wbr />html («Страница #4») — со следующим кодом:
Полная версия кода на JavaScript, который выполняется по событию onerror:
6–7. Браузер создает скриншот страницы, которая содержит iFrame с данными от облачного API. Например:
8. Затем браузер отправляет его на хост атакующего, но оба запроса не будут работать из‑за защиты SOP (изображение извлекается из localhost, в то время как текущий URL — http://<wbr />www.<wbr />attackers-website.<wbr />url). Тем не менее полученный HTML-код возвращается в headless-браузер («Страница #3»).
9–10. Тот же HTML-код отображается внутри вкладки браузера («Страница #3»), но на этот раз все запросы работают, потому что правила SOP не нарушаются (хост страницы такой же, как и у изображения, — localhost:3000).
11. Изображение с токеном отправляется атакующему.
Адрес http://<wbr />metadata.<wbr />google.<wbr />internal/<wbr />computeMetadata/<wbr />v1beta1/, который часто используется в примерах, устарел. В Google объявили, что скоро он перестанет отвечать и экземпляры Rendertron, работающие в Google Cloud, больше не будут так легко отдавать свои токены. В любом случае имей в виду, что методология и приемы этого исследования могут применяться не только для угона облачных токенов, но и для использования SSRF в целом.
Советы и трюки
Если не получается проэксплуатировать SSRF, но open redirect на странице есть, то можно провернуть XSS. Как упоминалось ранее, приложения для динамического рендеринга отсекают теги <<wbr />script> и ссылки на JavaScript, но код внутри атрибутов остается нетронутым, поэтому будет работать и приводить к XSS-перенаправлению вроде такого:
И никаких проблем с CORS, так как код выполнится по адресу страницы!
info
-
Rendertron и Prerender ищут в HTML специальные метатеги, которые используются для манипуляции с ответами. Это не уязвимость, но может использоваться как часть атаки, если атакующий имеет возможность встроить HTML в страницу и таким образом манипулировать ответом (например, переопределить X-Frame-Options или поменять один из заголовков CORS).
-
В обоих приложениях можно настраивать списки блокировок для запрашиваемых URL, но есть шанс обойти их с помощью трюков с DNS.
Вывод
Динамический рендеринг набирает популярность, и он будет использоваться все чаще, так как это разумный способ совместить использование современного JavaScript и SЕО. Google и другие компании продвигают этот подход, поэтому важно понять, какие слабости эта технология может принести.
Если ты в команде защиты, то будь в курсе, что headless-браузер внутри инфраструктуры может добавить много уязвимостей, если настроен неправильно. И даже самые маленькие огрехи в безопасности могут быть первым шагом к RCE. К счастью, много подобных мелочей можно найти с помощью современного статического анализа кода.
Если ты атакующий, используй новые знания во благо!
Если же ты разрабатываешь приложение с headless-браузером внутри, то еще раз порекламирую тебе Semgrep как средство для поиска багов в коде.