[Перевод] Как 9.3 уязвимость ждала открытия 3 года

Вступление

На днях мы с Ясером Алламом (Yasser Allam), он же _inzo, объединились, чтобы провести совместное исследование. Обсудив список потенциальных жертв целей, выбор пал на уже хорошо мне знакомый Next.js.

Next.js содержит более чем достаточно готовых функций и инструментов — отличная среда для исследования нюансов кода.

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

Оглавление
  1. Middleware в Next.js

  2. Логика авторизации: старый код — старое сокровище

  3. Эксплойты

    1. Обход авторизации

    2. Обход CSP

    3. DoS через Cache-Poisoning

  4. CVE-2025–29927

  5. Заключение

Middleware в Next.js

Middleware — механика, позволяющая обрабатывать запросы в момент между получением запроса от пользователя и до отправки сервером ответа. Основываясь на данных запроса, компонент middleware может изменить ответ сервера: редактировать тело ответа, изменять заголовки запроса/ответа или же создать свой ответ.

Будучи полноценным фреймворком, Next.js обладает собственным middleware — важной и широко используемой функцией. Стандартный компонент применяется в основном для:

  • редактирования путей;

  • переадресации на стороне сервера;

  • изменения заголовков (CSP, CORS и пр.)

  • и самое главное: аутентификации и авторизации.

Обычно авторизация реализуется через middleware, который ограничивает несанкционированный доступ к определённым путям.

Пример: Когда пользователь пытается получить страницу по пути /dashboard/team/admin, его запрос сначала обрабатывается middleware, который проверяет правомерность доступа и, в случае успешного прохождения проверки, пропускает запрос дальше; в противном случае middleware прервёт дальнейшую обработку запроса и, например, перенаправит пользователя на страницу для входа, в случае, если причиной отказа в доступе послужило отсутствие авторизационных данных.

Пример запроса к /dashboard/team/admin, который будет переадресован middleware на /dashboard
Пример запроса к /dashboard/team/admin, который будет переадресован middleware на /dashboard

Логика авторизации: старый код — старое сокровище

Как правильно сказал один великий человек, «talk is cheap, show me the bug» — закончим с теорией и перейдём к делу. Просматривая старую версию фреймворка (v12.0.7), наше внимание привлёк следующий фрагмент кода:

Фрагмент метода runMiddleware класса Server
Фрагмент метода runMiddleware класса Server

За вызов обработчиков middleware в Next.js отвечает метод runMiddleware, который, помимо своей основной функции, также работает с заголовком x-middleware-subrequest, используя его значения для определения, необходимо ли вызывать обработчик или нет.

Для этого из значения заголовка создаётся массив строк по разделителю :, а затем для каждого обработчика проверяется, не содержится ли его название (middlewareInfo.name) в этом списке

Проще говоря, если в запросе указать заголовок x-middleware-subrequest с правильным значением, middleware, независимо от его назначения, будет полностью проигнорирован, и запрос будет перенаправлен через NextResponse.next() далее, завершив свой путь в месте назначения, при этом никак не будучи изменённым/обработанным посредством middleware.

Заголовок x-middleware-subrequest является этаким мастер-ключом, позволяющим полностью обходить middleware. Уже на этом этапе мы понимали, что нашли нечто безумное, но нужно копнуть чуть глубже.

Чтобы наш «мастер-ключ» работал, нам нужно знать значение middlewareInfo.name, но что это вообще такое?

Порядок вызовов и middlewareInfo.name

Значение middlewareInfo.name вполне можно угадать: это всего лишь путь, по которому расположен файл с middleware-логикой. Чтобы понять решение актуальной проблемы, сделаем небольшой экскурс в историю, а конкретно в то, как настраивался middleware в старых версиях Next.js.

Во-первых, до версии 12.2 допустимым названием файла middleware являлось только _middleware.ts.

Во-вторых, app-роутер появился только в 13-й версии Next.js и на тот момент существовал только pages-роутер — до 13-й версии файл middleware должен был располагаться в папке pages (особенность роутера).

Эти знания позволяют определить точный путь к middleware, а значит, точно знать необходимое значение заголовка x-middleware-subrequest, которое состоит из имени директории и имени файла, по принятой в то время традиции начинать его с символа подчеркивания:

x-middleware-subrequest: pages/_middleware

И когда мы пытаемся обойти наш настроенный на безусловное перенаправление с /dashboard/team/admin на /dashboard middleware:

Пример удачного обхода middleware, а именно: получение контента пути /dashboard/team/admin
Пример удачного обхода middleware, а именно: получение контента пути /dashboard/team/admin

Итак, мы уже можем полностью обойти middleware, а значит, и любую систему защиты, основанную на нём. Это довольно дико, но есть некоторые нюансы.

Версии до 12.2 позволяли вложенным путям иметь проекту более одного _middleware.ts файла.

Видно, что в проекте имеется как минимум 2 файла с middleware-логикой, которые, к тому же, имеют различный порядок выполнения.
Видно, что в проекте имеется как минимум 2 файла с middleware-логикой, которые, к тому же, имеют различный порядок выполнения.

Что это значит для нашего эксплойта?

Каждая возможность разместить _middleware.ts файл в структуре проекта → +1 к необходимым параметрам заголовка.

Итак, чтобы получить доступ к защищённому middleware пути /dashboard/panel/admin, есть максимум три возможных значения middlewareInfo.name:

pages/_middleware
pages/dashboard/_middleware
pages/dashboard/panel/_middleware

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

Каково было наше удивление, когда через два дня после первой находки мы обнаружили, что все ≥11.1.4 версии Next.js были уязвимы! Код «мастер-ключа» перекочевал в другое место и логика эксплойта изменилась лишь слегка.

Внимательный читатель, должен был подметить, что с версии 12.2 название файла middleware больше не обязано было начинаться с подчёркивания — теперь он должен был называться просто middleware.ts. Кроме того, он теперь обязан не находиться в папке pages (что удобно для нас, поскольку появление app-роутера удвоило бы количество возможных путей).

Учитывая это, полезная нагрузка для версий ≥12.2 стала предельно проста:

x-middleware-subrequest: middleware

Ещё стоит упомянуть, что Next.js поддерживает организацию проекта с использованием директории /src:

В качестве альтернативы размещению специальных каталогов Next.js app или pages в корне вашего проекта, Next.js также поддерживает общепринятую схему размещения кода приложения в каталоге src.

В этом случае полезной нагрузкой будет:

x-middleware-subrequest: src/middleware

Таким образом, независимо от количества уровней в пути, возможны всего два (2) варианта. Это существенно упрощает применение эксплойта сразу для нескольких версий.

В более поздних версиях логика проверки снова немного меняется (в последний раз за статью).

Максимальная глубина рекурсии

Взглянем на код ниже:

v15.1.7
v15.1.7

Как и раньше, значение заголовка x-middleware-subrequest извлекается для создания массива по разделителю :. Однако в этот раз условие для того, чтобы запрос был передан напрямую, игнорируя правила middleware, отличается.

Длина полученного массива должна быть больше или равна значению константы MAX_RECURSION_DEPTH.

Поэтому для обхода middleware в версиях ≥15.1.7 нам всего лишь нужно добавить в запрос следующую нагрузку (в зависимости от того, используется ли в проекте /src):

x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware

или

x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware
f7d68ab7fd83211988fbd411dabff0ff.png

Зачем авторы добавили такую проверку?

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

Эксплойты

Ниже приведены примеры применения результатов исследований на реально существующих сайтах.

Обход авторизации

Пытаясь получить доступ к /admin/login, мы получаем ответ 404. Как видно из заголовка ответа, через middleware выполняется замена пути, чтобы предотвратить доступ неаутентифицированных пользователей к ресурсу:

404, переадресация на страницу «Не найдено»
404, переадресация на страницу «Не найдено»

А теперь с нашим заголовком:

200, получен доступ к контенту пути /admin/login
200, получен доступ к контенту пути /admin/login

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

Целевая версия Next.js: 15.1.7

Обход CSP

В этом примере сайт, среди прочего, использует middleware для установки CSP и cookies:

f3470ebe97d5e4674de2a5b2c78ffce1.png

Избавимся от CSP:

f057148e0194ef90a8348e0feaf65387.png

Целевая версия Next.js: 15.0.3.

Примечание

Обратите внимание на разницу в полезной нагрузке между двумя целями: одна из них использовала каталог src/, а другая — нет.

DoS через Cache-Poisoning

Да, данная уязвимость позволяет даже осуществить DoS через Cache-Poisoning.

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

Если мы обойдём middleware, то, следовательно, избежим переопределения пути с /. Т.к. разработчики не делали страницу на такой случай, мы получим ошибку 404 (или 500, в зависимости от конфигурации / типа перезаписи (полный url или только путь)).

Если сайт использует CDN, тогда можно принудительно кэшировать ответ 404, что существенно повлияет на доступность всего сайта.

518cf2f05c2df500a9c87324310a4873.png

Разъяснение

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

Проясняем ситуацию: уязвимым элементом является middleware. Если оно не используется (или, по крайней мере, не взаимодействует с конфиденциальными данными), то сильно беспокоиться не стоит: в примере DoS через Cache-Poisoning видно, что обход middleware не приводит к обходу каких-либо механизмов безопасности.

В противном случае последствия могут быть катастрофическими, и мы рекомендуем вам как можно скорее применить приведённые ниже меры по устранению уязви

CVE-2025–29927

Патчи

  • Для Next.js 15.x эта проблема исправлена в версии 15.2.3

  • Для Next.js 14.x эта проблема исправлена в версии 14.2.25

  • Для Next.js версий 11.1.4-13.5.6 мы рекомендуем использовать обходной путь (ниже).

Обходной путь

Если обновление до безопасной версии Next.js не представляется возможным, мы рекомендуем запретить все внешние запросы, который содержат заголовок `x-middleware-subrequest`.

Серьезность

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N (критично, 9.1/10)
CVSS:3.1/AV: N/AC: L/PR: N/UI: N/S: U/C: H/I: H/A: N (критично, 9.1/10)

https://github.com/vercel/next.js/security/advisories/GHSA-f82v-jwr5-mffw

На момент написания статьи приложения, которые хостятся на Vercel и Netlify, по-видимому, больше не уязвимы.

Заключение

Хронология:

27.02.2025

Отправлено письмо об уязвимости разработчикам (с уточнением, что уязвимыми были только версии между 12.0.0 и 12.0.7 — на тот момент мы именно так и думали)

01.03.2025

Отправлено второе письмо, в котором объяснялось, что уязвимыми являются все версии, включая последние стабильные релизы

05.03.2025

Получен ответ от команды Vercel, объясняющий, что версии 12.x больше не поддерживаются (вероятно, не было прочитано второе письмо/CVE)

05.03.2025

Отправлено ещё одно письмо с просьбой просмотреть второе письмо/CVE

11.03.2025

Отправлено ещё одно письмо с целью узнать, была ли учтена новая информация

17.03.2025

Получено подтверждающее получение новой информации письмо от команды Vercel

18.03.2025

Получено письмо от команды Vercel: отчёт был принят и уязвимость исправлена. Через несколько часов была выпущена версия 15.2.3, содержащая исправление

21.03.2025

Публикация CVE

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

Не стесняйтесь объединяться в команды: пересекать пустыню легче вместе.

© Habrahabr.ru