FaceHugger: שלוש חולשות ב-diffusers מריצות קוד גם כשכיביתם את ההגנה
מפתחת שרוצה להריץ מודל ליצירת תמונות מ-Hugging Face יודעת את הכלל: אם לא סומכים על המקור, טוענים אותו כשהדגל trust_remote_code כבוי, וכך הקוד שמגיע יחד עם המודל לא ירוץ. זה בדיוק מה שהדגל הזה אמור להבטיח. אלא שבספריית diffusers, הנפוצה ביותר לטעינת מודלים כאלה, הדגל הכבוי לא באמת עצר את הקוד.
חוקרי Zafran Security, גל זבן ואידו שני, פרסמו ב-27 ביולי מחקר שכינו FaceHugger: שלוש חולשות חמורות שמאפשרות למאגר מודל זדוני להריץ קוד שרירותי על כל מכונה שטוענת אותו, בלי אזהרה ובלי אישור. ההגנה שאמורה לחסום בדיוק את זה נבדקה, פשוט לא במקום הנכון בקוד. בהמשך נראה איך נראית הבדיקה השבורה, מה הטריק שהופך שם קובץ תמים למטען, ולמה זה חשוב הרבה מעבר לפרויקט אחד.
רקע: למה בכלל מריצים קוד של מישהו אחר
diffusers היא הספרייה שדרכה חלק גדול מעולם ה-AI טוען מודלים ליצירת תמונות, וידאו ואודיו, כמו Stable Diffusion. חלק מהמודלים לא מסתפקים במשקלים (קבצי המספרים של הרשת), אלא מגיעים עם קוד Python משלהם שמגדיר איך להריץ אותם, מה שנקרא custom pipeline. הקוד הזה שימושי, כי הוא מאפשר ליוצר המודל לצרף לוגיקה מותאמת. הוא גם קוד של זר שרץ אצלכם על המכונה.
בשביל זה קיים trust_remote_code. כשהוא כבוי, הספרייה אמורה לסרב לטעון ולהריץ קוד מותאם כזה, אלא אם המשתמש אישר במפורש שהוא סומך על המאגר. ההנחה הסבירה של כל מי שמשתמש בספרייה פשוטה: עם הדגל כבוי, טעינת מודל היא פעולה בטוחה. וזה חשוב, כי diffusers יורדת בערך שבעה מיליון פעם בחודש, קרוב ל-200 אלף בכל יום, והיא יושבת עמוק בתוך pipelines של AI בפרודקשן, מערכות CI/CD ותמונות קונטיינר (container images). כל מקום כזה טוען מודלים אוטומטית, בלי שאדם מסתכל על מה בדיוק נטען.
הבדיקה הנכונה, בזמן הלא נכון
שורש כל שלוש החולשות זהה, ומרגיז בפשטות שלו: הבדיקה אם מותר להריץ קוד מותאם רצה בשלב אחד, וטעינת הקוד עצמה קורית בשלב אחר. כשמודל נטען, diffusers פונה קודם להוריד את קובץ ההגדרות של המודל, ורק אחר כך פותרת איזו מחלקת pipeline להריץ ומאיפה. הבדיקה של trust_remote_code הוטמעה בתוך פונקציית ההורדה, DiffusionPipeline.download, ולא בנקודה שבה הקוד באמת נטען ומורץ.
המשמעות: כל מסלול קוד שמגיע לטעינה בלי לעבור דרך פונקציית ההורדה, מדלג על הבדיקה לגמרי. זה מה שכתוב מפורשות בעדכון האבטחה של הפרויקט, שההגנה הוטמעה בתוך פונקציית ההורדה במקום בנקודת טעינת המודול עצמה. התיקון בגרסה 0.38.0 העביר את הבדיקה לנקודה אחת, get_cached_module_file, שדרכה עוברת כל טעינת מודול, כך שאין יותר דלת צדדית לעקוף אותה.
איך שם קובץ אחד עוקף את ההגנה
הדוגמה החדה ביותר היא חולשה שסומנה CVE-2026-44827 (בחומרה 8.8). כשמשתמש טוען מודל בלי לציין pipeline מותאם, הפרמטר custom_pipeline מקבל את ערך ברירת המחדל None. בהמשך הקוד הערך הזה משורשר לתוך שם קובץ: הביטוי שבונה את שם הקובץ הופך את None למחרוזת, והתוצאה היא הקובץ None.py. הספרייה בודקת אם קובץ בשם הזה קיים במאגר, וזאת דרך מסלול קוד אחר, שלא מפעיל את בדיקת trust_remote_code.
הפרט המדויק, לפי עדכון האבטחה, יושב בקובץ pipeline_loading_utils.py: הפונקציה שפותרת איזו מחלקת pipeline להריץ מקבלת את הערך None בלי אימות, מרכיבה ממנו נתיב קובץ, ומריצה את מה שנמצא שם. קובץ None.py שהתוקף שתל מאפיל על מחלקות diffusers הרגילות ומחליף אותן בקוד שלו.
אז מה עושה תוקף? מעלה למאגר, לצד קובצי המודל התקינים, קובץ בשם None.py עם הקוד שלו. כל מי שיטען את המודל בצורה הרגילה, בלי pipeline מותאם ובלי לגעת בשום דגל, יריץ את הקובץ הזה. אין כאן שום פעולה חריגה מצד הקורבן. יש רק טעינת מודל שנראית תמימה לחלוטין, ומתחתיה קוד שרץ בשקט. וזה מה שהופך את החולשה הזאת למסוכנת כל כך: היא לא דורשת מהמשתמש לטעות, אלא רק לעשות בדיוק את מה שהתיעוד אומר לו לעשות.
מרוץ של שלוש עשיריות שנייה, ודלת אחורית מקומית
החולשה השנייה, CVE-2026-45804 (חומרה 7.5), מנצלת את הפער בזמן בין שתי הבקשות. אחרי שהבדיקה עברה על קובץ ההגדרות בבקשה הראשונה, יש חלון קצר עד שהקוד באמת נטען בבקשה השנייה. חוקרי Zafran מדדו חלון של כ-0.3 שנייה שבו אפשר להחליף את הקוד שנבדק בקוד אחר, זדוני. זו חולשה מסוג time-of-check to time-of-use (בדיקה ברגע אחד ושימוש ברגע אחר, כשמה שנבדק כבר אינו מה שרץ בפועל).
החולשה השלישית, CVE-2026-44513 (חומרה 8.8), מאגדת שלוש וריאציות של אותו שורש. באחת, הקוד המותאם נטען ממאגר של התוקף בזמן שהבדיקה בכלל התבצעה מול המאגר הראשי. בשנייה, טעינה מנתיב מקומי עוקפת את האימות ומאפשרת להזריק קוד מרוחק. בשלישית, החמורה מכולן מבחינה מעשית, קובצי Python בתוך תיקיות הרכיבים של מודל שכבר הורד, למשל unet/my_unet_model.py, רצים בלי בדיקה כשהמודל נטען מהאחסון המקומי. כלומר גם ארטיפקט שכבר יושב אצלכם על הדיסק, שהורד פעם בעבר ונראה מהימן, יכול להריץ קוד ברגע שטוענים אותו שוב.
מה תוקף מרוויח מזה
ברגע שקוד זר רץ בהקשר של תהליך שטוען מודל, הוא מקבל את כל ההרשאות של אותו תהליך. אם זה notebook של חוקר, התוקף מגיע לפרטי האימות ולקבצים שלו. אם זו מערכת CI/CD או שירות בפרודקשן, ההרשאות גדולות בהרבה: מפתחות ל-cloud, גישה למאגרי קוד, ואפשרות לנוע הלאה לתוך התשתית המחוברת. Infosecurity Magazine מונה את התרחישים שנפתחים בהצלחה: חילוץ פרטי אימות, הוצאת מידע, שינוי המערכת ותנועה לרשתות מחוברות.
זה בדיוק תרחיש של מתקפת שרשרת אספקה, רק שהחוליה החלשה היא מודל AI ולא ספריית קוד. מפתחים למדו לחשוד בחבילות תוכנה מהאינטרנט, אבל מודל שמורידים מ-Hugging Face עדיין מרגיש כמו נכס פסיבי, קובץ נתונים ששוקל כמה ג'יגה. שלוש החולשות האלה מראות שהוא לא. מאגר מודל הוא קוד שרץ, וצריך להתייחס אליו ככזה. וזה גם לא מקרה מבודד: trust_remote_code הוא מנגנון שחוזר בכל האקוסיסטם של Hugging Face, גם בספריית transformers, וכל מקום שבו הבדיקה שלו מופרדת מרגע ההרצה בפועל הוא מקום שראוי לבדיקה שנייה. הדיווח של The Arabian Post ממליץ במפורש לארגונים לבדוק לא רק סביבות פיתוח, אלא גם notebooks ותמונות קונטיינר שמקבעות גרסה ישנה של הספרייה.
כדאי לשים לב גם ללוח הזמנים. זבן ושני דיווחו על החולשות הראשונות עוד ב-19 במרץ, Hugging Face הוציאה את גרסה 0.38.0 שסוגרת אותן ב-1 במאי, והמחקר המלא פורסם רק בסוף יולי. פער כזה בין תיקון שקט לחשיפה פומבית הוא נורמלי בחשיפה אחראית, אבל הוא גם אומר שגרסאות עם החולשות הסתובבו בפרודקשן חודשים, ומי שלא עדכן מאז מאי עדיין חשוף.
מה זה אומר בפועל
התיקון קיים וזמין: diffusers בגרסה 0.38.0 ומעלה מעבירה את הבדיקה לנקודת הטעינה עצמה וסוגרת את כל הווריאציות. מי שמריץ AI בפרודקשן צריך לבדוק לא רק את סביבת הפיתוח, אלא כל מקום שטוען מודלים אוטומטית: pipelines, מערכות CI, ותמונות קונטיינר שאולי מקבעות גרסה ישנה של הספרייה. ובאשר למודלים עצמם, ההרגל הנכון הוא להתייחס לטעינת מודל ממקור לא מוכר בדיוק כמו להרצת קוד ממקור לא מוכר. כפי ש-FaceHugger מדגים, זה בדיוק מה שזה.
תגובות