تقوم Next.js Server Actions تلقائيًا بكشف كل دالة يتم تصديرها (exported function) في ملف يحتوي على وسم use-server كنقطة نهاية (endpoint) عامة لبروتوكول HTTP، مع تخصيص معرف فريد يستخدمه العميل لاستدعاء الدالة. توجد نقطة النهاية هذه سواء تم عرض زر أو رابط أو أي عنصر واجهة مستخدم آخر أم لا، مما يعني أن أي شخص يكتشف المعرف يمكنه استدعاء الدالة مباشرة.
يتعامل تصميم الإطار البرمجي مع Server Actions كدوال مساعدة عادية، ولكنها تصبح عند التشغيل (runtime) روابط URL يمكن الوصول إليها. بالنسبة للمطورين الذين افترضوا أن واجهة المستخدم (UI) هي الحارس الوحيد، فإن هذا يخلق سطح هجوم غير مرئي يمكن استغلاله بطلب واحد مصمم بدقة.
لماذا بدت Server Actions آمنة
تم تقديم Server Actions للسماح للمطورين بكتابة كود من جانب الخادم (server-side) بجانب مكوناتهم مباشرة، مما يتجنب التعقيدات الروتينية (boilerplate) لمسارات API المنفصلة. يبدو الاستخدام النموذجي كالتالي:
<form action={deleteInvoice}>
<button type="submit">Delete</button>
</form>
نظرًا لأن النموذج هو الطريقة الوحيدة المرئية لتشغيل deleteInvoice ، يقوم العديد من المطورين بإخفاء الزر عن المستخدمين غير المصرح لهم، ويعتقدون أن تواقيع TypeScript ستمنع البيانات المشوهة، ويعتمدون على حقيقة أن الدالة تعيش في وحدة (module) مخصصة للخادم فقط. لا توفر أي من هذه الافتراضات حماية حقيقية.
التعرض الخفي
عندما يحتوي ملف على “use server” ، يقوم Next.js بتجميع كل دالة مصدرة في نقطة نهاية مثل:
POST /_next/data/<build-id>/<page>.json?__rsc=<action-id>
الـ <action-id> هو هاش (hash) مستقر يتم تضمينه في حزمة العميل (client bundle). يمكن للمهاجم الحصول عليه عن طريق:
- فحص حركة مرور الشبكة للصفحة.
- قراءة ملف JavaScript المجمع (المعرف عبارة عن سلسلة نصية بسيطة).
- التخمين بناءً على اصطلاحات التسمية إذا كان المشروع يتبع أنماطًا يمكن التنبؤ بها.
بمجرد معرفة المعرف، يمكن إرسال طلب من أي أداة — مثل cURL أو Postman أو نص برمجي خبيث — متجاوزًا أي فحوصات على مستوى واجهة المستخدم.
ثلاثة مخاطر ملموسة
| الخطر | لماذا يهم الأمر |
|---|---|
| فحوصات واجهة المستخدم غير فعالة | إخفاء زر أو رابط لا يحذف نقطة النهاية الأساسية. تظل نقطة النهاية قابلة للوصول، تمامًا مثل صفحة مسؤول مخفية لا تزال موجودة على الخادم. |
| TypeScript لا يوفر أمانًا أثناء التشغيل | يتم تجريد الأنواع (types) عند تشغيل الكود. الدالة المعلنة كـ deleteInvoice(id: number) يمكن أن تستقبل سلسلة نصية ضخمة، أو مصفوفة، أو حتى JSON خبيث، مما يؤدي إلى أخطاء منطقية أو هجمات حقن (injection attacks). |
| لا يوجد مصادقة أو تفويض افتراضي | تبدو Server Actions كمساعدات محلية، لذا غالبًا ما ينسى المطورون إضافة فحوصات الجلسة (session checks)، أو حماية CSRF، أو فحوصات الأذونات على مستوى الصف (row-level permission checks) التي تعد معيارية في مسارات API التقليدية. |
كيفية تأمين Server Action
- مصادقة المتصل – تحقق من وجود جلسة أو رمز (token) صالح قبل تشغيل أي منطق عمل.
- التحقق من صحة الحمولة (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_ أبدًا. |
| إعادة التوجيه المفتوحة (Open redirects) | قبول معلمة استعلام redirect ودمجها بسذاجة يمكن أن يرسل المستخدمين إلى //evil.com. |
تحقق من الهدف مقابل قائمة مسموح بها (whitelist) أو فرض فحوصات نفس الأصل (same-origin). |
| تزوير الطلبات من جانب الخادم (SSRF) | جلب عنوان URL يقدمه المستخدم يمكن أن يسمح للمهاجمين بالوصول إلى الخدمات الداخلية أو نقاط نهاية بيانات وصفية سحابية (cloud metadata endpoints). | ضع أسماء المضيفين في قائمة مسموح بها، واحظر نطاقات IP الخاصة، وقم بتعيين مهلات زمنية (timeouts). |
dangerouslySetInnerHTML |
عرض HTML المقدم من المستخدم دون تطهير (sanitisation) يفتح ثغرات XSS. | استخدم مكتبة مثل DOMPurify أو تجنب استخدام HTML الخام تمامًا. |
الفحص التلقائي لهذه الأنماط
يمكن لأدوات التحليل الساكن (Static-analysis tools) تحديد البنى الخطرة المذكورة أعلاه. أحد الخيارات خفيفة الوزن هو Semgrep، الذي يعمل بسرعة ويمكن دمجه في خطوط أنابيب CI:
npx --yes semgrep --config https://raw.githubusercontent.com/catidegla/stacksec/main/rules .
تتضمن مجموعة القواعد فحوصات للدوال البرمجية المصدرة للخادم، وإساءة استخدام NEXT_PUBLIC_ ، وإعادة التوجيه المفتوحة، وأنماط SSRF، وإدراج HTML غير الآمن.
ما يجب مراقبته لاحقًا
- تحديثات إطار العمل – راقب إصدارات Next.js؛ فقد يقدم الفريق خطافات (hooks) مصادقة مدمجة أو يعزل (sandbox) نقاط النهاية التي يتم إنشاؤها.
- أدوات المجتمع – تظهر حالياً إضافات ESLint جديدة وقواعد Semgrep خاصة بـ Next.js لأتمتة الضمانات الموضحة هنا.
- حوادث من الواقع العملي – مع اعتماد المزيد من المشاريع لـ Server Actions، راقب الثغرات المكتشفة التي توضح المخاطر في الممارسة العملية. يمكن للاكتشاف المبكر أن يوجه مراجعات الأمان الداخلية قبل حدوث أي اختراق.
الخلاصة: إن Server Action في Next.js ليس مجرد دالة مساعدة خاصة؛ بل هو نقطة نهاية HTTP عامة بمجرد تصديره. تعامل معه مثل أي مسار API آخر — قم بالمصادقة، والتحقق من الصحة، والتفويض — وإلا فإن سهولة كتابة كود الخادم بجانب واجهة المستخدم قد تتحول سريعاً إلى ثغرة أمنية.
