Next.js Server Actions חושפים באופן אוטומטי כל פונקציה מיוצאת (exported) בקובץ use-server כנקודת קצה (endpoint) ציבורית של HTTP, ומקצים לה מזהה ייחודי שהלקוח (client) משתמש בו כדי להפעיל את הפונקציה. נקודת הקצה קיימת בין אם כפתור, קישור או כל רכיב UI אחר רונדרו ובין אם לאו, מה שאומר שכל מי שיגלה את המזהה יכול לקרוא לפונקציה ישירות.
העיצוב של המסגרת (framework) מתייחס ל-Server Actions כאל פונקציות עזר רגילות, אך בזמן ריצה הן הופכות לכתובות URL נגישות. עבור מפתחים שהניחו שה-UI הוא השומר היחיד, הדבר יוצר שטח תקיפה בלתי נראה שניתן לנצל באמצעות בקשה אחת מתוכננת (crafted request).
למה Server Actions נראו בטוחים
Server Actions הוכנסו כדי לאפשר למפתחים לכתוב קוד בצד השרת (server-side) ממש לצד הרכיבים שלהם, תוך הימנעות מהתבנית (boilerplate) של נתיבי API נפרדים. שימוש טיפוסי נראה כך:
<form action={deleteInvoice}>
<button type="submit">Delete</button>
</form>
מכיוון שהטופס הוא הדרך הגלויה היחידה להפעיל את deleteInvoice, מפתחים רבים מסתירים את הכפתור עבור משתמשים לא מורשים, חושבים שחתימות TypeScript יעצרו נתונים לא תקינים, ומסתמכים על העובדה שהפונקציה נמצאת במודול של שרת בלבד (server-only module). אף אחת מההנחות הללו אינה מספקת הגנה אמיתית.
החשיפה הנסתרת
כאשר קובץ מכיל “use server”, Next.js מקמפל כל פונקציה מיוצאת לנקודת קצה כגון:
POST /_next/data/<build-id>/<page>.json?__rsc=<action-id>
ה-<action-id> הוא hash יציב שחבילת הלקוח (client bundle) מטמיעה. תוקף יכול להשיג אותו על ידי:
- בדיקת תעבורת הרשת של הדף.
- קריאת ה-JavaScript המאוגד (ה-ID הוא מחרוזת פשוטה).
- ניחוש המבוסס על מוסכמות שיום (naming conventions) אם הפרויקט עוקב אחר דפוסים צפויים.
ברגע שה-ID ידוע, ניתן לשלוח בקשה מכל כלי — cURL, Postman, או סקריפט זדוני — תוך עקיפת כל הבדיקות ברמת ה-UI.
שלושה סיכונים קונקרטיים
| סיכון | למה זה חשוב |
|---|---|
| בדיקות UI אינן יעילות | הסתרת כפתור או קישור אינה מוחקת את נקודת הקצה שבבסיס. נקודת הקצה נותרת נגישה, בדיוק כמו דף ניהול (admin) נסתר שעדיין קיים בשרת. |
| TypeScript אינו מציע בטיחות בזמן ריצה | הטיפוסים (Types) מוסרים כאשר הקוד רץ. פונקציה שהוגדרה כ-deleteInvoice(id: number) יכולה לקבל מחרוזת ענקית, מערך, או אפילו JSON זדוני, מה שמוביל לשגיאות לוגיקה או להתקפות הזרקה (injection attacks). |
| אין אימות (authentication) או הרשאה (authorization) כברירת מחדל | Server Actions נראים כמו פונקציות עזר מקומיות, ולכן מפתחים שוכחים לעיתים קרובות להוסיף בדיקות סשן, הגנת CSRF, או בדיקות הרשאות ברמת השורה (row-level) שהן סטנדרטיות בנתיבי API מסורתיים. |
איך לאבטח Server Action
- אמת את הקורא (Authenticate the caller) – וודא שקיים סשן או טוקן תקף לפני הרצת כל לוגיקה עסקית.
- אמת את המטען (Validate the payload) – השתמש בספריית סכימה (למשל, Zod, Yup) כדי לאכוף טיפוסי נתונים ומגבלות ערכים בזמן ריצה.
- אשר את הפעולה (Authorize the operation) – מעבר לשאלה "האם המשתמש מחובר?", וודא שהמשתמש הוא הבעלים של הרשומה הספציפית שהוא מנסה לשנות או למחוק.
דוגמה מינימלית:
'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 ושרשור (concatenating) נאיבי שלו עלולה לשלוח משתמשים ל-//evil.com. |
אמת את היעד מול רשימה לבנה (whitelist) או אכוף בדיקות של אותו מקור (same-origin). |
| Server-Side Request Forgery (SSRF) | שליפת URL המסופק על ידי משתמש עלולה לאפשר לתוקפים להגיע לשירותים פנימיים או לנקודות קצה של מטא-דאטה בענן. | אפשר רשימת מארחים (allow-list), חסום טווחי IP פרטיים, והגדר זמני קצוב (timeouts). |
dangerouslySetInnerHTML |
רינדור של HTML המסופק על ידי משתמש ללא ניקוי (sanitisation) פותח פתח ל-XSS. | השתמש בספרייה כמו DOMPurify או הימנע לחלוטין משימוש ב-HTML גולמי. |
סריקה אוטומטית של דפוסים אלו
כלי ניתוח סטטי (static-analysis) יכולים לסמן את המבנים המסוכנים שפורטו לעיל. אפשרות קלה אחת היא Semgrep, שרצה במהירות וניתנת לשילוב בצינורות CI (CI pipelines):
npx --yes semgrep --config https://raw.githubusercontent.com/catidegla/stacksec/main/rules .
סט החוקים כולל בדיקות עבור פונקציות שרת מיוצאות, שימוש לרעה ב-NEXT_PUBLIC_, הפניות פתוחות, דפוסי SSRF והכנסת HTML לא בטוחה.
מה לעקוב בהמשך
- עדכוני פריימוורק – עקבו אחר גרסאות Next.js; הצוות עשוי להציג hooks מובנים לאימות (authentication) או לבצע sandboxing ל-endpoints שנוצרים.
- כלי קהילה – תוספים (plugins) חדשים של ESLint וכללי Semgrep ספציפיים ל-Next.js מופיעים כדי לאוטומציה של מנגנוני ההגנה המתוארים כאן.
- אירועים מהעולם האמיתי – ככל שיותר פרויקטים מאמצים Server Actions, עקבו אחר פרצות (exploits) שמתפרסמות וממחישות את הסיכון בפועל. זיהוי מוקדם יכול לסייע בסקירות אבטחה פנימיות לפני שמתרחשת פריצה.
שורה תחתונה: Next.js Server Action אינו פונקציית עזר פרטית; הוא הופך ל-HTTP endpoint ציבורי ברגע שאתם מייצאים אותו. התייחסו אליו כמו לכל API route אחר – בצעו אימות (authenticate), וולידציה (validate) והרשאה (authorize) – אחרת, הנוחות שבכתיבת קוד שרת לצד ה-UI עלולה להפוך במהירות לסיכון אבטחה.
