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

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

Почему Server Actions казались безопасными

Server Actions были внедрены, чтобы позволить разработчикам писать серверный код прямо рядом со своими компонентами, избегая шаблонного кода (boilerplate) для отдельных API-маршрутов. Типичный пример использования:

<form action={deleteInvoice}>
  <button type="submit">Delete</button>
</form>

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

Скрытая уязвимость

Когда файл содержит “use server”, Next.js компилирует каждую экспортируемую функцию в эндпоинт вида:

POST /_next/data/<build-id>/<page>.json?__rsc=<action-id>

<action-id> — это стабильный хеш, который встраивается в клиентский бандл. Злоумышленник может получить его следующими способами:

  • Изучая сетевой трафик страницы.
  • Читая скомпилированный JavaScript (ID представляет собой обычную строку).
  • Подбирая его на основе соглашений об именовании, если проект следует предсказуемым паттернам.

Как только ID станет известен, запрос можно отправить с помощью любого инструмента — cURL, Postman или вредоносного скрипта, — минуя любые проверки на уровне интерфейса.

Три конкретных риска

Риск Почему это важно
Проверки в UI неэффективны Скрытие кнопки или ссылки не удаляет сам эндпоинт. Эндпоинт остается доступным, подобно скрытой странице администратора, которая все еще существует на сервере.
TypeScript не обеспечивает безопасность во время выполнения Типы удаляются при выполнении кода. Функция, объявленная как deleteInvoice(id: number), может получить огромную строку, массив или даже вредоносный JSON, что приведет к логическим ошибкам или инъекциям.
Отсутствие аутентификации или авторизации по умолчанию Server Actions выглядят как локальные помощники, поэтому разработчики часто забывают добавить проверку сессии, защиту от CSRF или проверку прав доступа на уровне строк, которые являются стандартом для традиционных API-маршрутов.

Как обезопасить Server Action

  1. Аутентифицируйте вызывающую сторону — проверяйте наличие валидной сессии или токена перед выполнением любой бизнес-логики.
  2. Валидируйте полезную нагрузку — используйте библиотеки схем (например, Zod, Yup) для принудительной проверки типов данных и ограничений значений во время выполнения.
  3. Авторизуйте операцию — помимо проверки «авторизован ли пользователь?», подтвердите, что пользователь владеет конкретной записью, которую он пытается изменить или удалить.

Минимальный пример:

'use server';
import { getSession } from '@/auth';
import { z } from 'zod';
import { db } from '@/db';

const DeleteInvoiceSchema = z.object({
  id: z.number().int().positive(),
});

export async function deleteInvoice(formData: FormData) {
  const session = await getSession();
  if (!session) throw new Error('Unauthenticated');

  const parsed = DeleteInvoiceSchema.safeParse({
    id: Number(formData.get('id')),
  });
  if (!parsed.success) throw new Error('Invalid input');

  const invoice = await db.invoice.findUnique({ where: { id: parsed.data.id } });
  if (!invoice || invoice.ownerId !== session.userId) {
    throw new Error('Unauthorized');
  }

  await db.invoice.delete({ where: { id: invoice.id } });
}

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

Другие тонкие ловушки Next.js

Проблема Симптом Решение
Переменные окружения NEXT_PUBLIC_ Все, что имеет префикс NEXT_PUBLIC_, попадает в клиентский бандл, раскрывая секреты. Храните секреты в обычных переменных окружения, никогда не добавляйте к ним префикс NEXT_PUBLIC_.
Открытые редиректы Принятие параметра запроса redirect и его наивная конкатенация может перенаправить пользователей на //evil.com. Проверяйте целевой URL по белому списку или внедряйте проверки на соответствие тому же источнику (same-origin).
Server-Side Request Forgery (SSRF) Загрузка URL, предоставленного пользователем, может позволить злоумышленникам получить доступ к внутренним сервисам или эндпоинтам метаданных облака. Используйте белый список хостов, блокируйте диапазоны частных IP-адресов и устанавливайте таймауты.
dangerouslySetInnerHTML Рендеринг предоставленного пользователем HTML без санитайзинга открывает возможность для XSS. Используйте библиотеку вроде DOMPurify или полностью избегайте использования сырого HTML.

Автоматический поиск таких паттернов

Инструменты статического анализа могут помечать перечисленные выше рискованные конструкции. Один из легких вариантов — Semgrep, который работает быстро и может быть интегрирован в CI-конвейеры:

npx --yes semgrep --config https://raw.githubusercontent.com/catidegla/stacksec/main/rules .

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

Что изучить дальше

  • Обновления фреймворка – Следите за релизами Next.js; команда может внедрить встроенные хуки аутентификации или изолировать создаваемые эндпоинты.
  • Инструментарий сообщества – Появляются новые плагины ESLint и специфичные для Next.js правила Semgrep для автоматизации мер защиты, описанных здесь.
  • Реальные инциденты – По мере того как всё больше проектов внедряют Server Actions, следите за раскрытыми эксплойтами, которые иллюстрируют риски на практике. Раннее обнаружение поможет провести внутренние проверки безопасности до того, как произойдет взлом.

Главный вывод: Server Action в Next.js — это не приватная вспомогательная функция; это публичный HTTP-эндпоинт с того самого момента, как вы его экспортируете. Относитесь к нему как к любому другому API-маршруту — проводите аутентификацию, валидацию и авторизацию — иначе удобство написания серверного кода рядом с UI может быстро превратиться в угрозу безопасности.