Next.js Server Actions автоматично відкривають кожну експортовану функцію у файлі use-server як публічну HTTP-ендпоінт, призначаючи їй унікальний ідентифікатор, який клієнт використовує для виклику функції. Ендпоінт існує незалежно від того, чи відображається кнопка, посилання чи будь-який інший елемент інтерфейсу, а це означає, що будь-хто, хто дізнається ідентифікатор, може викликати функцію безпосередньо.
Дизайн фреймворку розглядає Server Actions як звичайні допоміжні функції, але під час виконання вони стають доступними URL-адресами. Для розробників, які вважали, що інтерфейс (UI) є єдиним охоронцем, це створює невидиму поверхню атаки, яку можна експлуатувати за допомогою одного спеціально сформованого запиту.
Чому 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> — це стабільний хеш, який вбудовується в клієнтський бандл. Зловмисник може отримати його шляхом:
- Перегляду мережевого трафіку сторінки.
- Читання зібраного (bundled) JavaScript (ID є звичайним рядком).
- Вгадування на основі конвенцій іменування, якщо проєкт слідує передбачуваним шаблонам.
Щойно ідентифікатор відомий, запит можна надіслати за допомогою будь-якого інструменту — cURL, Postman або шкідливого скрипта, — обходячи будь-які перевірки на рівні інтерфейсу.
Три конкретні ризики
| Ризик | Чому це важливо |
|---|---|
| Перевірки на рівні UI неефективні | Приховування кнопки або посилання не видаляє базовий ендпоінт. Ендпоінт залишається доступним, як прихована сторінка адміністратора, яка все ще існує на сервері. |
| TypeScript не забезпечує безпеки під час виконання | Типи видаляються під час виконання коду. Функція, оголошена як deleteInvoice(id: number), може отримати величезний рядок, масив або навіть шкідливий JSON, що призведе до логічних помилок або ін'єкційних атак. |
| Відсутність автентифікації або авторизації за замовчуванням | Server Actions виглядають як локальні допоміжні функції, тому розробники часто забувають додати перевірку сесії, захист від CSRF або перевірку прав доступу на рівні рядків, які є стандартними для традиційних API-маршрутів. |
Як захистити Server Action
- Автентифікуйте викликаючого користувача – перевірте наявність дійсної сесії або токена перед виконанням будь-якої бізнес-логіки.
- Валідуйте корисне навантаження (payload) – використовуйте бібліотеку схем (наприклад, Zod, Yup) для забезпечення відповідності типів даних та обмежень значень під час виконання.
- Авторизуйте операцію – окрім перевірки «чи увійшов користувач у систему?», підтвердьте, що користувач володіє саме тим записом, який він намагається змінити або видалити.
Мінімальний приклад:
'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. |
Перевіряйте ціль за білим списком або застосовуйте перевірки того самого походження (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; команда може запровадити вбудовані хуки автентифікації або ізолювати (sandbox) згенеровані ендпоінти.
- Інструментарій спільноти – З'являються нові плагіни ESLint та специфічні для Next.js правила Semgrep, щоб автоматизувати заходи безпеки, описані тут.
- Реальні інциденти – Оскільки все більше проєктів впроваджують Server Actions, стежте за розкритими експлойтами, які ілюструють ризики на практиці. Раннє виявлення допоможе провести внутрішні перевірки безпеки до того, як станеться злам.
Висновок: Next.js Server Action — це не приватна допоміжна функція; це публічний HTTP-ендпоінт з моменту, коли ви його експортуєте. Ставтеся до нього як до будь-якого іншого API-маршруту — автентифікуйте, валідуйте та авторизуйте — інакше зручність написання серверного коду поруч із UI може швидко перетворитися на загрозу безпеці.
