Next.js Server Actions بهطور خودکار هر تابع صادرشده (exported) در یک فایل use-server را به عنوان یک نقطه اتصال (endpoint) عمومی HTTP در دسترس قرار میدهند و یک شناسه منحصربهفرد به آن اختصاص میدهند که کلاینت از آن برای فراخوانی تابع استفاده میکند. این نقطه اتصال چه دکمه، لینک یا هر عنصر رابط کاربری (UI) دیگری رندر شده باشد و چه نشده باشد، وجود دارد؛ به این معنی که هر کسی که شناسه را پیدا کند، میتواند مستقیماً تابع را فراخوانی کند.
طراحی این فریمورک با Server Actions مانند توابع کمکی معمولی برخورد میکند، اما در زمان اجرا (runtime)، آنها به URLهای قابل دسترس تبدیل میشوند. برای توسعهدهندگانی که تصور میکردند رابط کاربری (UI) تنها دروازهبان است، این موضوع یک سطح حمله (attack surface) نامرئی ایجاد میکند که میتواند با یک درخواست دستکاریشده (crafted request) مورد سوءاستفاده قرار گیرد.
چرا Server Actions امن به نظر میرسیدند
Server Actions برای این معرفی شدند که به توسعهدهندگان اجازه دهند کدهای سمت سرور را مستقیماً در کنار کامپوننتهای خود بنویسند و از نوشتن کدهای تکراری (boilerplate) برای مسیرهای API جداگانه اجتناب کنند. یک نمونه استفاده معمول به این صورت است:
<form action={deleteInvoice}>
<button type="submit">Delete</button>
</form>
از آنجایی که فرم تنها راه قابل مشاهده برای اجرای deleteInvoice است، بسیاری از توسعهدهندگان دکمه را برای کاربران غیرمجاز مخفی میکنند، تصور میکنند که امضاهای TypeScript مانع از ورود دادههای نادرست میشوند، و به این واقعیت تکیه میکنند که تابع در یک ماژول مخصوص سرور (server-only) قرار دارد. هیچکدام از این فرضها محافظت واقعی ایجاد نمیکنند.
افشای پنهان
وقتی فایلی حاوی “use server” باشد، Next.js هر تابع صادرشده را به یک نقطه اتصال (endpoint) مانند زیر کامپایل میکند:
POST /_next/data/<build-id>/<page>.json?__rsc=<action-id>
شناسه <action-id> یک هش (hash) پایدار است که در باندل کلاینت تعبیه شده است. یک مهاجم میتواند آن را از طریق روشهای زیر به دست آورد:
- بررسی ترافیک شبکه صفحه.
- خواندن جاوااسکریپت باندل شده (شناسه یک رشته ساده است).
- حدس زدن بر اساس قراردادهای نامگذاری، اگر پروژه از الگوهای قابل پیشبینی پیروی کند.
به محض اینکه شناسه مشخص شود، میتوان درخواستی را از هر ابزاری — مانند cURL، Postman یا یک اسکریپت مخرب — ارسال کرد و تمام بررسیهای سطح UI را دور زد.
سه ریسک ملموس
| ریسک | چرا اهمیت دارد |
|---|---|
| بررسیهای UI بیاثر هستند | مخفی کردن یک دکمه یا لینک، نقطه اتصال زیرین را حذف نمیکند. نقطه اتصال همچنان قابل دسترس باقی میماند، درست مانند یک صفحه ادمین مخفی که هنوز روی سرور وجود دارد. |
| TypeScript امنیت زمان اجرا فراهم نمیکند | تایپها هنگام اجرای کد حذف میشوند. تابعی که به صورت deleteInvoice(id: number) تعریف شده است، میتواند یک رشته بسیار طولانی، یک آرایه یا حتی یک JSON مخرب دریافت کند که منجر به خطاهای منطقی یا حملات تزریق (injection attacks) میشود. |
| عدم وجود احراز هویت یا تعیین سطح دسترسی پیشفرض | Server Actions شبیه به توابع کمکی محلی به نظر میرسند، بنابراین توسعهدهندگان اغلب فراموش میکنند که بررسیهای نشست (session)، محافظت CSRF یا بررسیهای سطح دسترسی ردیفی (row-level) را که در مسیرهای 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_ شروع شود، در باندل کلاینت قرار میگیرد و باعث افشای اطلاعات حساس (secrets) میشود. |
اطلاعات حساس را در متغیرهای محیطی معمولی نگه دارید و هرگز آنها را با NEXT_PUBLIC_ شروع نکنید. |
| ریدایرکتهای باز (Open redirects) | پذیرش یک پارامتر پرسوجوی redirect و چسباندن ساده آن میتواند کاربران را به //evil.com هدایت کند. |
مقصد را با یک لیست سفید (whitelist) اعتبارسنجی کنید یا بررسیهای هممنشأ (same-origin) را اعمال کنید. |
| جعل درخواست سمت سرور (SSRF) | واکشی (fetching) یک URL که توسط کاربر ارائه شده است، میتواند به مهاجمان اجازه دهد به سرویسهای داخلی یا نقاط اتصال متادیتای ابری دسترسی پیدا کنند. | نامهای میزبان (hostnames) را در لیست مجاز قرار دهید، محدودههای IP خصوصی را مسدود کنید و زمانهای انتظار (timeouts) تعیین کنید. |
dangerouslySetInnerHTML |
رندر کردن HTML ارائه شده توسط کاربر بدون پاکسازی (sanitisation)، راه را برای حملات XSS باز میکند. | از کتابخانهای مانند DOMPurify استفاده کنید یا کلاً از HTML خام خودداری کنید. |
اسکن خودکار این الگوها
ابزارهای تحلیل ایستا (Static-analysis) میتوانند ساختارهای پرخطر ذکر شده در بالا را شناسایی کنند. یک گزینه سبک، Semgrep است که به سرعت اجرا میشود و میتواند در خط لولههای CI ادغام شود:
npx --yes semgrep --config https://raw.githubusercontent.com/catidegla/stacksec/main/rules .
این مجموعه قوانین شامل بررسی توابع سرور صادرشده، استفاده نادرست از NEXT_PUBLIC_، ریدایرکتهای باز، الگوهای SSRF و درج ناامن HTML است.
آنچه در ادامه باید مراقب باشید
- بهروزرسانیهای فریمورک – انتشار نسخههای Next.js را دنبال کنید؛ ممکن است تیم توسعه، هوکهای احراز هویت داخلی معرفی کند یا نقاط پایانی (endpoints) تولیدشده را در محیط ایزوله (sandbox) قرار دهد.
- ابزارهای جامعه کاربری – پلاگینهای جدید ESLint و قوانین Semgrep مخصوص Next.js در حال ظهور هستند تا اقدامات حفاظتی توصیفشده در اینجا را خودکارسازی کنند.
- حوادث دنیای واقعی – با گسترش استفاده از Server Actions در پروژهها، مراقب اکسپلویتهای (exploits) فاششده باشید که ریسکها را در عمل نشان میدهند. شناسایی زودهنگام میتواند پیش از وقوع یک رخنه، به بازبینیهای امنیتی داخلی کمک کند.
نکته کلیدی: یک Next.js Server Action یک تابع کمکی خصوصی نیست؛ به محض اینکه آن را export کنید، یک HTTP endpoint عمومی است. با آن مانند هر مسیر API دیگر برخورد کنید — یعنی احراز هویت، اعتبارسنجی و تعیین سطح دسترسی (authorize) را انجام دهید — در غیر این صورت، راحتیِ نوشتن کد سرور در کنار UI میتواند به سرعت به یک ریسک امنیتی تبدیل شود.
