הכותרת מגיעה מהכתובת, והשרת מצייר אותה: חולשה קריטית ב-Next.js הופכת תמונת תצוגה מקדימה להרצת קוד
כששולחים קישור בוואטסאפ או בטוויטר, מה שנפתח שם לפני האתר עצמו הוא תמונה: כותרת הכתבה על רקע מעוצב, לפעמים לוגו, לפעמים שם הכותב. באתרים רבים שבנויים ב-Next.js התמונה הזאת לא מחכה מוכנה על השרת. היא נבנית ברגע שמישהו מבקש אותה, והטקסט שנכנס לתוכה נלקח מהכתובת שאליה פנו.
ביום שלישי פרסמה Vercel, שמפתחת את Next.js, עדכון אבטחה מחוץ למחזור הרגיל שקובע שהמסלול הזה עלול להסתיים בהרצת קוד על השרת. החולשה נוגעת למימוש של ImageResponse שרץ על Node.js, אותו רכיב שמייצר את תמונות התצוגה המקדימה, והיא קיימת בגרסאות 16.2.0 עד 16.3.5. התיקון הוא 16.3.6, ו-Vercel מדרגת את החומרה כקריטית.
נעבור על מה שקורה בין ה-JSX שכתבתם לבין ה-PNG שיוצא בצד השני, איפה נשברת השרשרת, ולמה שלושת המקומות שבהם מפתח היה מצפה לראות נורה אדומה על חולשה כזאת שותקים בינתיים.
מה קורה בין הקוד לתמונה
ImageResponse נותן למפתח לכתוב תמונה כמו שכותבים עמוד: JSX, קצת CSS, טקסט שמגיע ממשתנה. מתחת לזה מתרחש תרגום בשני שלבים. קודם הפריסה שכתבתם, כלומר הטקסט, הצבעים והמיקומים, מומרת לפורמט גרפי שמתאר צורות ואותיות בתור קוד ולא בתור פיקסלים. הפורמט הזה הוא SVG. רק אחר כך רכיב אחר לוקח את ה-SVG ומרנדר אותו לתמונה אמיתית שאפשר לשלוח לדפדפן.
התיעוד הרשמי מפרט מי עושה מה: הספרייה Satori, שמוגדרת בעצמה כספרייה להמרת HTML ו-CSS ל-SVG, אחראית על השלב הראשון, ורכיב בשם Resvg סוגר את השני. הכול קורה בתוך התהליך של האפליקציה, על השרת, בזמן שהבקשה מחכה.
הסיבה שהטקסט בתמונה מגיע מבחוץ היא לא רשלנות אלא התכנון עצמו. אתר עם אלפי עמודים לא מייצר אלפי תמונות ידנית, אלא מחזיק נתיב אחד שמקבל את הכותרת כפרמטר ומצייר אותה. הנתיב הזה, מעצם הגדרתו, פתוח לכל דורש ולא דורש התחברות, כי מי שאמור לפנות אליו הוא הסורק של וואטסאפ, של לינקדאין ושל מנועי החיפוש. זה שילוב לא נוח: הקלט מגיע מהעולם, והעיבוד שלו קורה על השרת.
הבעיה היא לא התמונה, היא הטקסט
SVG הוא קוד, לא תמונה במובן הרגיל. כשמכניסים לתוכו מחרוזת שהגיעה מבחוץ, צריך לוודא שהתווים שיש להם משמעות מבנית בפורמט, בעיקר סוגריים משולשים ומרכאות, יומרו לצורה שמוצגת כטקסט רגיל. לפעולה הזאת קוראים escaping. בלעדיה, מחרוזת שנראית כמו תגית נקראת בסוף בתור תגית.
זה בדיוק מה שקרה כאן. "בתנאים מסוימים, escaping לקוי בפלט ה-SVG ש-Satori מייצרת עלול להוביל להרצת קוד מרחוק, בגלל חולשות בתלויות upstream אחרות", נכתב בהודעה של Vercel, ומיד אחר כך: "התיקון משדרג את אותן תלויות".
שימו לב למה שהמשפט הזה אומר ולמה שהוא לא אומר. ה-escaping החסר הוא לא החולשה שמריצה את הקוד, הוא מה שפותח את הדלת: ברגע שערך שהתוקף שולט בו נקרא כמבנה SVG ולא כטקסט, הוא מגיע לרכיב הבא בשרשרת בצורה שאף אחד לא תכנן. אילו חולשות יש ברכיב הבא, Vercel לא אמרה. היא לא נקבה בשמן של התלויות ששודרגו, והתיעוד רק מספר מי מקבל את ה-SVG אחרי ש-Satori מסיימת איתו.
ההבדל בין שתי נקודות המבט האלה מופיע גם בציונים. The Hacker News, שקרא את שתי הודעות האבטחה, מדווח ששתיהן מתייחסות לאותה רשומת CVE-2026-94545, אבל ההודעה של Next.js נותנת לה 9.5 בחומרה קריטית, בעוד ההודעה של Satori עצמה נותנת 5.3 בלבד ומסבירה שההשפעה תלויה במה שעושים עם פלט ה-SVG. שתיהן צודקות. בספרייה בודדת זו בעיית תצוגה, ובצינור שמזרים את התוצאה ישר הלאה זו הרצת קוד.
על ההודעה של Vercel חתומים שלושה אנשים מהצוות, Josh Story, Karim Rahal ו-Sebastian Silbermann, והיא מפנה את מי שמוצא חולשות דומות לתוכנית התגמולים שהחברה מפעילה על פרויקטי הקוד הפתוח שלה. מי דיווח על החולשה הזאת בפועל לא נאמר.
אז למה דווקא Node.js, ולמה לא Edge?
ל-ImageResponse יש שני מימושים, אחד שרץ על Node.js ואחד שרץ על סביבת ה-Edge, והחולשה נמצאת רק בראשון. Vercel מציינת במפורש שאפליקציות שמשתמשות במימוש ה-Edge אינן מושפעות.
הבעיה היא שהתצורה הבטוחה כאן היא גם זו שנמצאת בדרך החוצה. בתיעוד של הגדרות הניתוב רשום ש-runtime מקבל את הערכים nodejs או edge, שברירת המחדל היא nodejs, ושהערך edge מסומן כמיושן. כלומר מי שלא נגע בהגדרה הזאת, וזה הרוב, נמצא בדיוק במימוש הפגיע, והאלטרנטיבה שלא נפגעה היא זו שהפרויקט כבר לא ממליץ עליה.
למה הכלים הרגילים שותקים
נניח שאתם בודקים עכשיו אם אתם חשופים. יש שלוש דרכים מקובלות, ונכון להיום שלושתן לא יעזרו.
הראשונה היא להסתכל ב-lockfile ולחפש את Satori. בדקנו את המטא-דאטה של החבילה ב-npm, ורשימת התלויות של next בגרסה 16.3.5 הפגיעה זהה לחלוטין לזו של 16.3.6 המתוקנת: postcss, @next/env, styled-jsx, @swc/helpers וכמה נוספות. Satori פשוט לא מופיעה שם, כי היא ארוזה בתוך חבילת Next.js ולא נמשכת כתלות נפרדת. ה-lockfile שלכם לא יגלה לכם כלום, וגם השוואת התלויות בין הגרסאות לא תראה מה שודרג.
השנייה היא להריץ npm audit. לפי הבדיקה של The Hacker News ב-23 בספטמבר, הכלי לא סימן את גרסה 16.3.5 הפגיעה.
השלישית היא לחפש את הרשומה במאגרים. גם כאן בדקנו בעצמנו באותו יום: ל-CVE-2026-94545 אין עדיין רשומה ב-NVD, מאגר החולשות של ממשלת ארצות הברית, וגם OSV, המאגר שמרכז חולשות של ספריות קוד פתוח, לא מחזיר דבר עבור אף אחת משתי הודעות האבטחה. סורק שמסתמך על המאגרים האלה לא רואה כאן חולשה.
מה שכן עובד זו הדרך הפשוטה. תפתחו את package.json ותסתכלו על המספר.
מה עוד לא ידוע
הקוד הפגיע לא הגיע אתמול. לפי מרשם ה-npm, גרסה 16.2.0 פורסמה ב-18 במרץ, כלומר החולשה הייתה זמינה כחצי שנה לפני שתוקנה. התיקון עצמו יצא מהר: Satori 0.33.5 עלתה ב-22 בספטמבר בשעה 16:01 לפי שעון UTC, ו-Next.js 16.3.6 בשעה 16:19, שמונה עשרה דקות אחריה.
מה שאין הוא דרך להסתכל אחורה. Vercel לא פרסמה סימנים שמאפשרים לבדוק אם נתיב פגיע באתר שלכם נוצל לפני העדכון, ולא אמרה אם אפליקציות שמתארחות אצלה מוגנות ממילא. The Hacker News מדווח שנכון ל-23 בספטמבר לא היו דיווחים פומביים על תקיפות שמשתמשות בחולשה ולא פורסם קוד הדגמה. היעדר דיווחים הוא לא אותו דבר כמו היעדר תקיפות, במיוחד כשאין מה לחפש בלוגים.
נקודה אחת שכדאי לשים לב אליה היא גבול ההשפעה. לפי הדיווח, הדוגמה שמופיעה בהודעת האבטחה מכניסה ערך מכתובת הבקשה לתוך אלמנט title בתוך ה-SVG, אבל ההודעה לא אומרת אם גם טקסט שיושב באלמנטים רגילים נספר. מי שמנסה להחליט אם נתיב מסוים אצלו נחשב פגיע נשאר בינתיים בלי תשובה ברורה.
מה זה אומר בפועל
אם יש לכם אפליקציית Next.js בגרסה 16.2 ומעלה, חפשו ייבוא של ImageResponse מתוך next/og, בעיקר בקבצי route handler ובקבצי opengraph-image, ובדקו אם משהו מהבקשה מגיע לתוך התמונה. השדרוג ל-16.3.6 הוא התיקון היחיד: בענף 16.2 אין גרסה מתוקנת, ומי שנמצא שם צריך לעבור ל-16.3.6 ולא לחכות לתיקון מקומי. גרסה 15.5.26 מביאה חיזוקים לענף 15.5, אבל לפי Vercel ענף 15 לא היה חשוף להרצת הקוד מלכתחילה.
מי שמשתמש ב-Satori ישירות, בלי Next.js, צריך את גרסה 0.33.5. ואם השדרוג נדחה לשבוע הבא מסיבה כלשהי, ההנחיה הזמנית היא להוציא ערכים שמגיעים מבחוץ מתוך התוכן, המאפיינים והסגנונות שנכנסים ל-SVG. זה פחות נוח מלעדכן מספר אחד בקובץ.
תגובות