[Перевод] Как 9.3 уязвимость ждала открытия 3 года
Вступление
На днях мы с Ясером Алламом (Yasser Allam), он же _inzo
, объединились, чтобы провести совместное исследование. Обсудив список потенциальных жертв целей, выбор пал на уже хорошо мне знакомый Next.js.
Next.js содержит более чем достаточно готовых функций и инструментов — отличная среда для исследования нюансов кода.
Будучи преисполненными энтузиазма, верой, любопытством и, конечно же, терпением, мы принялись ковыряться в исходном коде фреймворка.
Middleware в Next.js
Логика авторизации: старый код — старое сокровище
Эксплойты
Обход авторизации
Обход CSP
DoS через Cache-Poisoning
CVE-2025–29927
Заключение
Middleware в Next.js
Middleware — механика, позволяющая обрабатывать запросы в момент между получением запроса от пользователя и до отправки сервером ответа. Основываясь на данных запроса, компонент middleware может изменить ответ сервера: редактировать тело ответа, изменять заголовки запроса/ответа или же создать свой ответ.
Будучи полноценным фреймворком, Next.js обладает собственным middleware — важной и широко используемой функцией. Стандартный компонент применяется в основном для:
редактирования путей;
переадресации на стороне сервера;
изменения заголовков (CSP, CORS и пр.)
и самое главное: аутентификации и авторизации.
Обычно авторизация реализуется через middleware, который ограничивает несанкционированный доступ к определённым путям.
Пример: Когда пользователь пытается получить страницу по пути
/dashboard/team/admin
, его запрос сначала обрабатывается middleware, который проверяет правомерность доступа и, в случае успешного прохождения проверки, пропускает запрос дальше; в противном случае middleware прервёт дальнейшую обработку запроса и, например, перенаправит пользователя на страницу для входа, в случае, если причиной отказа в доступе послужило отсутствие авторизационных данных.

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

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:

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

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

Что это значит для нашего эксплойта?
Каждая возможность разместить _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) варианта. Это существенно упрощает применение эксплойта сразу для нескольких версий.
В более поздних версиях логика проверки снова немного меняется (в последний раз за статью).
Максимальная глубина рекурсии
Взглянем на код ниже:

Как и раньше, значение заголовка 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

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

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

/admin/login
Мы можем получить доступ к конечной странице без каких-либо проблем, при этом middleware полностью игнорируется.
Целевая версия Next.js: 15.1.7
Обход CSP
В этом примере сайт, среди прочего, использует middleware для установки CSP и cookies:

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

Целевая версия Next.js: 15.0.3
.
Обратите внимание на разницу в полезной нагрузке между двумя целями: одна из них использовала каталог src/
, а другая — нет.
DoS через Cache-Poisoning
Да, данная уязвимость позволяет даже осуществить DoS через Cache-Poisoning.
Представим, что есть сайт, который переопределяет пути запросов по фактору их географического положения, добавляя (/en, /fr и т.п.), при этом не предоставляя страницу по пути /
.
Если мы обойдём middleware, то, следовательно, избежим переопределения пути с /
. Т.к. разработчики не делали страницу на такой случай, мы получим ошибку 404 (или 500, в зависимости от конфигурации / типа перезаписи (полный url или только путь)).
Если сайт использует CDN, тогда можно принудительно кэшировать ответ 404, что существенно повлияет на доступность всего сайта.

Разъяснение
С момента предупреждения об уязвимости мы получили несколько сообщений от обеспокоенных состоянием своих приложений и не понимающих масштабов атаки разработчиков.
Проясняем ситуацию: уязвимым элементом является 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`.
Серьезность

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 |
В заключение хочется сказать, что поиск уязвимостей нулевого дня радует и заряжает адреналином только тогда, когда появляется хоть какая-то зацепка; всё остальное время — это довольно сомнительное, долгое путешествие, которое вознаграждает знаниями только достаточно любопытных.
Не стесняйтесь объединяться в команды: пересекать пустыню легче вместе.