Серверна функція з 'use server' виглядає як звичайна функція, яку викликає компонент. Насправді Next.js створює для неї ендпойнт, доступний прямим POST-запитом - не лише з вашого інтерфейсу.
'use server';
export async function deletePost(postId: string) {
await db.post.delete({ where: { id: postId } }); // будь-хто видалить будь-який пост
}
Що Next.js робить для захисту сам:
- зашифровані недетерміновані ідентифікатори дій, що змінюються між збірками;
- видалення невикористаних дій з клієнтського бандла;
- шифрування змінних із замикань (значень, захоплених функцією, оголошеною всередині компонента) - вони ходять клієнтом і назад, але в зашифрованому вигляді. Ключ генерується на кожну збірку (для кількох серверів - спільний
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY); - перевірка джерела: заголовок
Originпорівнюється зHost- захист від CSRF; для проксі-конфігурацій -serverActions.allowedOrigins.
Але документація прямо застерігає: це зменшує ризики, а автентифікацію й авторизацію треба перевіряти всередині кожної дії.
Правильно:
'use server';
export async function deletePost(postId: string) {
const session = await auth();
if (!session) throw new Error('Unauthorized');
const post = await db.post.findUnique({ where: { id: postId } });
if (!post || post.authorId !== session.user.id) throw new Error('Forbidden'); // захист від IDOR
await db.post.delete({ where: { id: postId } });
revalidatePath('/posts');
}
Типові помилки:
- перевірка лише на сторінці: сторінка доступна лише адміну, тож «дії на ній безпечні». Ні - перевірка доступу до сторінки не поширюється на дії, оголошені в ній;
- довіра аргументам:
postId, ціна, роль приходять від клієнта й можуть бути будь-якими. Валідація (Zod) обов'язкова, як для будь-якого API; - повернення зайвого: повернене значення потрапляє в браузер - лише потрібні поля;
- покладатися на шифрування замикань для секретів - документація не радить; секрети не повинні опинитися в замиканні взагалі.
Рекомендований підхід - шар доступу до даних: модуль з import 'server-only', де зібрано перевірки прав і запити до бази. Серверні дії лишаються тонкими й викликають його. Так перевірки не залежать від того, звідки викликано код.
Аналогія з Laravel: серверна дія - це маршрут контролера. Ніхто не робить контролер без authorize() лише тому, що на нього немає посилання в інтерфейсі.