Главная / Статьи

Почему в Server Actions Next.js требуется авторизация внутри каждого тела функции

Анализ конкретного случая захвата учетной записи показывает, как неавторизованные функции Next.js Server Actions позволяют выполнять привилегированные операции, и где необходимо разместить проверку авторизации для предотвращения этого.

Издатель: REACTAPP.TOP4 мин чтения994 слов

Источники: Unauthenticated Next.js Server Actions Leading to Zero Click Account Takeover

Server Actions в Next.js позволяют компоненту React напрямую вызывать функцию сервера без посредничества API-маршрута. Такое удобство устраняет единственное место, где многие команды привыкли выполнять проверку авторизации — обработчик маршрута. Когда такая проверка не переносится в тело функции вместе с логикой, функцию может вызвать любой, кто знает её идентификатор. Приведённый ниже пример демонстрирует именно такую проблему на реальном портале подписок, где два незащищённых действия были связаны между собой, что привело к безударному захвату учётной записи, а затем описываются защитные правила, которые могли бы этому помешать.

Как на самом деле выполняются Server Actions

В рамках App Router функция сервера представляет собой функцию на сервере, которая вызывается непосредственно из компонента. Фреймворк присваивает каждой такой функции 40-значный шестнадцатеричный идентификатор, включает этот идентификатор в пакет для клиента и преобразует вызов в запрос POST с заголовком Next-Action. Ключевой момент заключается в том, что обработка происходит на основе этого заголовка, а не пути URL; тело запроса представляет собой просто аргументы функции, переданные в порядке их указания.

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

Первое действие: входящее письмо, выходящая конфиденциальная информация

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

POST /particuliers/integrale-web-mensuel-2116 HTTP/2
Host: subdomain.target.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
Next-Action: 42a24b711d12f695d798b371e38b584c551ebb6a
Content-Type: text/plain;charset=UTF-8
Accept: text/x-component
["victim@gmail.com",true]

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

{
"user_id": "auth0|000000000000000000000001",
"email": "researcher@example.com",
"identities": [
{ "provider": "auth0", "connection": "www-target-com" }
],
"app_metadata": {
"subscriber_number": "900000000",
"custom_user_id": "00000000–0000–4000–8000–000000000000"
}
}

Форма этого пакета данных является ключевым признаком. Поля вроде identities и app_metadata не предоставляются публичным API аутентификации поставщика идентичностей; они берутся из его API управления, административного интерфейса, к которому можно получить доступ с помощью токена межмашинного взаимодействия, имеющего такие полномочия, как чтение и обновление данных пользователей. Получение этих данных через неавторизованный запрос означает, что на сервере находился токен с повышенными привилегиями, который тайно использовался анонимной функцией. Помимо самого факта обнаружения, в ответе также передавались два значения, необходимые для дальнейших действий: идентификатор пользователя и имя подключения.

Второе действие: изменение пароля любого пользователя

Второе действие принимало объект и строку. В объекте хранились пароль и информация о подключении; строка представляла собой идентификатор пользователя. При отправке запроса на тестовый аккаунт без аутентификации он выглядел следующим образом.

POST / HTTP/2
Host: subdomain.target.com
Next-Action: 89abcdef0123456789abcdef0123456789abcdef
Content-Type: text/plain;charset=UTF-8
Accept: text/x-component
[{"password":"<REDACTED>","connection":"www-target-com"},"auth0|000000000000000000000001"]

Этот запрос ничего не доказал: не было ни куки сессии, ни информации о существующем пароле, ни токена сброса или подтверждения по электронной почте, ни запроса на усиление безопасности. Сервер ответил с сообщением об успехе и обновленным объектом пользователя. Решающая ошибка заключается в том, откуда берется идентификатор пользователя: он является вторым аргументом, передаваемым вызывающим кодом, вместо того чтобы браться из сессии или поля subject подписанного токена. Функция изменяет тот аккаунт, о котором ей сообщают.

Та же операция, другое поле, украденный путь восстановления

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

POST / HTTP/2
Host: subscribe.target.com
Next-Action: 89abcdef0123456789abcdef0123456789abcdef
Content-Type: text/plain;charset=UTF-8
Accept: text/x-component
[{"email":"attacker@example.com","connection":"www-target-com"},"auth0|000000000000000000000001"]

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

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