פונקציה אחת שאמרה "לא" בשני מובנים שונים, והמפענח המשיך לפענח: כך הגיעה הרצת קוד ל-Node.js דרך Brotli
בתוך container נקי שהריץ Node.js 24.22.0, חוקרי Imperva שלחו לאפליקציית בדיקה גוף בקשה אחד דחוס, ומיד אחר כך הופיע על הדיסק קובץ שהם חיפשו. באפליקציה עצמה לא היה שום קוד שיוצר אותו. הפרסום של Imperva, שכתב רון מסס ופורסם ב-22 באוקטובר, מראה הרצת קוד על build רשמי של Node.js, בלי debugger ובלי קומפילציה מיוחדת לצורך הניסוי. לשרשרת הזאת הם קוראים Node2Shell.
עדכוני האבטחה של Node.js יצאו יום לפני כן, ב-21 באוקטובר, וסגרו עשר חולשות. זו שמעניינת כאן היא CVE-2026-111497, והכתובת האמיתית שלה היא לא Node.js: היא יושבת ב-Brotli, ספריית הדחיסה של גוגל, ש-Node.js אורז לתוכו עותק שלה. לפי רשומת ה-CVE שגוגל פרסמה הטווח הנגוע הוא Brotli 1.0.3 עד 1.2.0, "בכל הפלטפורמות". בהמשך נראה מה בדיוק שבור במפענח, מה Imperva הצליחה להדגים ומה לא, ולמה שני גופים רשמיים דירגו את אותה חולשה אחרת לגמרי.
למה דווקא Brotli, ולמה היא נמצאת בכל מקום
Brotli היא שיטת דחיסה שגוגל פרסמה ב-2015, והיא מה שעומד מאחורי Content-Encoding: br שכל דפדפן מודרני מציע בכל בקשה. היא דוחסת טוב יותר מ-gzip, ולכן כמעט כל שרת web, CDN וספריית HTTP תומכים בה היום. המימוש עצמו הוא ספרייה ב-C, והיא מהסוג שאף אחד לא מתקין במכוון. היא פשוט מגיעה ארוזה בתוך דברים אחרים.
ב-Node.js זה מפורש. הפרויקט לא נשען על Brotli של מערכת ההפעלה אלא אורז עותק משלו לתוך הבינארי, ומריץ אותו גם עבור node:zlib וגם עבור תשובות שמגיעות דחוסות ל-fetch() המובנה. המשמעות המעשית חדה: גרסת Brotli שרצה אצלכם היא לא משהו שמופיע ב-package.json, היא נקבעת לפי גרסת Node.js, וכלים שסורקים תלויות לא רואים אותה בכלל.
אז למה המפענח המשיך לפענח?
כדי להבין את החולשה צריך דבר אחד על הפורמט. זרם Brotli מחולק למקטעים, ובתוך כל מקטע אפשר להחליף את טבלאות הקידוד באמצע. הזרם מכריז מראש כמה "סוגי בלוק" הוא מתכוון להשתמש בהם בכל אחת משלוש הקטגוריות שלו, ואם הוא מכריז על יותר מאחד, הוא חייב לספק גם את הטבלאות שאומרות מתי לעבור מסוג לסוג וכמה זמן כל סוג נמשך. לפקודה שמחליפה סוג באמצע קוראים block switch.
והנה החלק שעליו הכול נשען: אם מקטע מכריז על סוג בלוק אחד בלבד, המפענח מדלג לגמרי על קריאת הטבלאות האלה. הוא לא בונה עץ Huffman לסוגי בלוק ולא קורא את מונה האורך, כי בזרם תקין לא תופיע שם אף פעם פקודת החלפה. במקטע כזה block switch הוא לא קלט חריג שצריך לטפל בו בעדינות. הוא מצב שהפורמט שולל מראש.
בגרסאות שלפני התיקון, הפונקציה שמטפלת בהחלפה, DecodeBlockTypeAndLength בקובץ c/dec/decode.c, בדקה בדיוק את זה ובדקה אותו ראשון: אם יש פחות משני סוגי בלוק, היא חזרה עם BROTLI_FALSE. הבעיה היא שאותו BROTLI_FALSE שימש אותה גם לדבר אחר לגמרי, "נגמרו לי הביטים באמצע סמל, תחזור אליי כשיהיה עוד קלט". ערך חזרה בוליאני אחד, שני מצבים שאין ביניהם קשר, והקורא לא יכול להבחין ביניהם.
מה שקרה אצל הקורא תלוי במסלול. במסלול הזהיר, זה שמיועד לזרם שעשוי להגיע בחלקים, המאקרו BROTLI_SAFE תרגם את ה-false הזה ישר ל-BROTLI_DECODER_NEEDS_MORE_INPUT, שמר את המצב וביקש עוד בייטים. במסלול המהיר המצב היה גרוע יותר: שם הפונקציות העוטפות הוגדרו כ-void, כלומר התשובה נזרקה בלי שאף אחד הסתכל עליה, והפענוח המשיך לשורה הבאה כאילו ההחלפה הצליחה.
והוא המשיך עם מצב פנימי שאין לו שום אחיזה בזרם: מונה אורך בלוק שנשאר אפס, וטבלאות שלא נבנו מעולם. מכאן זו כתיבה מחוץ לתחום. הניסוח ברשומת ה-CVE של גוגל הוא "שיבוש של זיכרון מצב המפענח הסמוך", וזה בדיוק התיאור, המפענח דורך על המבנים של עצמו.
התיקון שגוגל שחררה ב-Brotli 1.2.1 הוא בעצם הודאה שחסרה מילה באוצר המילים. הפונקציות האלה מחזירות עכשיו BrotliDecoderErrorCode במקום בוליאני, והפרויקט הוסיף קוד שגיאה שלא היה קיים קודם, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH, שמספרו מינוס 17. השוואה של decode.c בין התג v1.2.0 לתג v1.2.1 מראה את זה בבירור: אחרי התיקון, "אין מספיק קלט" ו"הפורמט שבור" סוף סוף אומרים שני דברים שונים. ה-changelog של Brotli 1.2.1 רושם את CVE-2026-111497 בשורה אחת ובלי פירוט, וגרסאות התיקון בשלושת הקווים הן 1.0.10, 1.1.1 ו-1.2.1.
מה Imperva הדגימה, ומה היא אומרת שלא
עד כאן מה שבור בספרייה. השאלה המעשית היא איך קלט עוין מגיע אליה בכלל, וכאן Imperva בדקה שני מסלולים. הראשון הוא תשובה דחוסה שהאפליקציה שלכם מורידה בעצמה דרך fetch() המובנה, כלומר השרת שאליו פניתם הוא זה שמחליט מה ייכנס למפענח. השני הוא גוף בקשה נכנסת. שרת HTTP חשוף של Node.js לא מפרסר גוף Brotli רק מפני שהוא קיבל אותו, אבל Express 5 כן: ה-body parsers שלו, ובהם raw() ו-json(), מפענחים Brotli כשהם מוחלים על route. בשני המסלולים הקוד שנשלח בתוך הנתונים רץ על 24.22.0, ועל 24.22.1 אותו קלט נדחה והשרת נשאר למעלה.
הגרסאות שנבדקו, לפי Imperva, הן Express בטווח 5.0.0 עד 5.3.0 ו-body-parser בטווח 2.0.0 עד 2.3.0, כשהבדיקות נעשו על גרסאות מוצמדות ולא על כל שחרור שביניהן. שווה לקרוא גם את מה שהחוקרים אומרים על גבולות העבודה שלהם, כי הוא לא שולי: הכול נבדק על image רשמי של Node.js ל-Linux בארכיטקטורת arm64, וארכיטקטורות אחרות וקווי 22.x ו-26.x לא נבדקו כלל. גם ההרצה עצמה לא הצליחה בכל ניסיון. Imperva כותבת שהיא "דרשה כמה ניסיונות" בסביבה שלהם, בלי לנקוב באחוז הצלחה או במספר הניסיונות.
וזה הקשר שכדאי להחזיק כשמודדים את הקצב כאן. ה-PoC הציבורי הראשון, שפורסם ב-22 באוקטובר, הגיע להרצת קוד רק על build שהוחלש בכוונה לצורך הבדיקה; על build רגיל המחבר שלו דיווח על קריסה ועל קריאה מוגבלת של זיכרון התהליך, ולא על הרצת קוד. ההדגמה של Imperva היא זו שסגרה את הפער, באותו יום. נכון ל-22 באוקטובר, לפי Imperva ולפי הדיווחים, לא דווח על ניצול של החולשה במתקפות אמיתיות.
9.2 או High? תלוי את מי שואלים
לאותה חולשה יש כרגע שתי דרגות חומרה רשמיות. דף העדכונים של Node.js מדרג אותה High ולא נותן ציון מספרי בכלל, והניסוח שלו לקוני עד כדי אי-מידע, "פגם ב-Node.js שעלול לגרום לכתיבה מחוץ לתחום במפענח Brotli שמאפשרת הרצת קוד מרחוק". גוגל, שהיא ה-CNA של Brotli, רשמה בצד שלה ציון CVSS 9.2 בסולם 4.0 ודירוג Critical.
הווקטור עצמו מסביר את הפער טוב יותר מכל טיעון. גוגל סימנה בו AT:P, כלומר Attack Requirements: Present, השדה שאומר שהתקיפה תלויה בתנאים שלא תמיד מתקיימים. זה בדיוק מה ש-Imperva דיווחה מהשטח, הרצה שלא עובדת בכל ניסיון. שני הגופים מתארים אותה תמונה ורק מתמצתים אותה למספר אחר. נכון ל-23 באוקטובר הרשומה ב-NVD עדיין במצב Awaiting Analysis, כך שהציון היחיד שקיים הוא זה של גוגל.
מי מצא את זה, ובאיזו מהירות
כאן יש פרט ששווה לעצור עליו. לפי דף העדכונים של Node.js, מי שדיווחו על החולשה הם Khai Tran ו-Sylvie Mayer מ-OpenAI Codex Security, צוות האבטחה של מודל הקוד של OpenAI. הדף לא מפרט איך הם הגיעו אליה, ולכן אין כאן יותר ממה שכתוב. את התיקון בצד Node.js עשו Matteo Collina ו-Antoine du Hamel, והערות השחרור של 24.22.1 מראות את החלוקה המדויקת: commit אחד שמביא את התיקון של Evgenii Kliuchnikov מ-Brotli עצמה, ועוד commit של du Hamel שכל תפקידו לחשוף את קוד השגיאה החדש כלפי חוץ.
(אגב, הפרסום של Imperva מייחס את הדיווח ל-Collina. דף העדכונים של Node.js, המקור הרשמי, מפריד בין מי שדיווח לבין מי שתיקן.)
הנקודה שמסס סוגר בה את הפרסום היא שהמהירות היא הסיפור: לדבריו, מהירות התיקון חייבת לעמוד בקצב של הכלים שהופכים את הניצול למהיר יותר. בפועל, בין שחרור התיקון להדגמת הרצת קוד על build רשמי עבר יום אחד.
מה זה אומר בפועל
מה שצריך לעשות פשוט: לעדכן ל-22.23.4, ל-24.22.1 או ל-26.12.1. מי שעל קו 20.x ומטה לא יקבל תיקון, כי הקו יצא מתמיכה, ושם צריך לעבור קו שלם. שווה לזכור שהגרסה שקובעת כאן היא של הרנטיים ולא של חבילה, אז בדקו את גרסת Node.js בתוך כל image ועל כל host, כי סריקת תלויות לא תמצא את Brotli הארוז בבינארי. ואם אתם מסתמכים בינתיים על WAF, העדכון של Vercel, שהוסיף חוקים ב-21 באוקטובר, אומר במפורש את מה שחשוב להבין: חוקים על תעבורה נכנסת לא מכסים תשובות דחוסות שהקוד שלכם מוריד בעצמו משרת אחר. את המסלול הזה סוגר רק עדכון של הרנטיים.
תגובות