ב-16 בספטמבר בשעה 17:45 מישהו העלה לפורום הדלפות ארכיון עם קוד המקור של CrowdSec, החברה שמפתחת מנוע קוד פתוח לזיהוי וחסימה של כתובות IP זדוניות. כשהצוות פתח את הארכיון הוא מצא בו פרט מוזר: הקומיט האחרון בכל המאגרים היה מ-22 במאי, 01:24 UTC. הקוד לא נגנב באותו שבוע. הוא ישב אצל מישהו ארבעה חודשים לפני שפורסם.

ב-18 בספטמבר פרסמה החברה דוח תחקיר מלא שסוגר את המעגל ומצביע על נקודת הכניסה: המחשב של עובד שעזב את החברה, שנדבק במתקפת שרשרת האספקה על חבילות ה-npm של TanStack בחודש מאי. בהמשך נעבור על איך נראתה המתקפה המקורית, מה היא חיפשה על מחשבי מפתחים, ולמה התחקיר נתקע דווקא בלוגים של GitHub.

שלוש חולשות שאף אחת מהן לא מספיקה לבד

TanStack היא משפחת ספריות קוד פתוח פופולרית לבניית ממשקי web ב-JavaScript. בדוח שפרסמו המתחזקים הם מתארים איך ב-11 במאי, בין 19:20 ל-19:26 UTC, תוקף פרסם 84 גרסאות זדוניות ב-42 חבילות ‎@tanstack/*‎. אף token של npm לא נגנב בדרך, ותהליך הפרסום עצמו לא נפרץ. זה מה שהופך את המקרה למעניין.

ההתחלה הייתה ב-workflow אוטומטי שבודק את גודל ה-bundle בכל בקשת מיזוג. ה-workflow הזה רץ בטריגר בשם pull_request_target, כלומר הוא מופעל בהקשר של המאגר המקורי ולא של ה-fork ששלח את הקוד, ולכן יש לו גישה למשאבים של המאגר גם כשהקוד שהוא בודק נכתב על ידי זר לגמרי. לתבנית הזאת קוראים בקהילה Pwn Request, והיא מתועדת כבר שנים.

התוקף עבד לאט. ב-10 במאי ב-17:16 הוא יצר fork של המאגר ושינה את שמו ל-zblgg/configuration, כדי שחיפוש ברשימת ה-forks לא יעלה אותו. באותו לילה הוא דחף לשם קומיט זדוני עם קובץ בן כ-30 אלף שורות, תחת זהות מפוברקת בשם claude ובכתובת no-reply של GitHub (המתחזקים מדגישים בדוח שאין לזה קשר ל-Claude של Anthropic), ופתח את הודעת הקומיט ב-[skip ci] כדי שהדחיפה עצמה לא תפעיל בדיקות. למחרת ב-11:11 הוא דחף את הקומיט הזה לראש בקשת המיזוג, ומשימת הבדיקה עשתה בדיוק את מה שהיא אמורה לעשות: משכה את הקוד של ה-fork והריצה עליו build.

ומכאן החלק היפה. ב-11:29 נשמרה ל-cache של GitHub Actions ספריית חבילות מורעלת בנפח 1.1 ג'יגה-בייט, תחת מפתח שחושב בדיוק כך שה-workflow של הפרסום יחפש אותו בפעם הבאה שירוץ על ה-main. שתי דקות אחר כך התוקף החזיר את בקשת המיזוג לגרסה הנקייה של ה-main, סגר אותה ומחק את הענף. מי שהסתכל על הבקשה ראה שינוי של אפס קבצים. ההרעלה נשארה.

שני פרטים הופכים את זה לאפשרי, ושניהם התנהגות מתועדת ולא באג: שמירת ה-cache בסוף המשימה לא נחסמת על ידי הגדרת ההרשאות של ה-workflow, כי היא משתמשת ב-token פנימי של ה-runner, ותחום ה-cache משותף לכל המאגר, כך שהרצה של בקשת מיזוג יכולה לדרוס רשומות שהרצות הייצור ישלפו אחר כך.

איך מ-cache מורעל מגיעים להרשאת פרסום

ל-workflow של הפרסום ב-TanStack יש הרשאה להנפיק זהות זמנית מול npm, מנגנון שנקרא trusted publishing ונועד בדיוק כדי שלא יצטרכו להחזיק token קבוע. כשה-cache המורעל שוחזר, קבצים של התוקף כבר ישבו על הדיסק של ה-runner והורצו במהלך ה-build. הם איתרו את התהליך של ה-runner דרך רשימת התהליכים במערכת, קראו את הזיכרון שלו, וחילצו משם את ה-token שה-runner מנפיק בזיכרון בלבד. עם ה-token הזה ביד הם פנו ישירות לשרת החבילות של npm.

התוצאה היא פרסום שנראה לגמרי חוקי מצד הרישום, ושלא עבר בכלל דרך שלב הפרסום המוגדר ב-workflow. השלב הזה, אגב, בכלל לא רץ: הבדיקות נכשלו והוא דולג. "התוקף לא המציא שיטות חדשות, הוא הרכיב מחדש מחקר שכבר פורסם", כתבו המתחזקים, והצביעו על כך שסקריפט חילוץ הזיכרון הועתק מילה במילה, כולל שורת הקרדיט בהערה, מפריצה לרכיב tj-actions/changed-files במרץ 2025.

מה שהציל את כולם היה מזל, וגם את זה הדוח אומר בגלוי: המטען שיבש את הבדיקות, ולכן הרעש התגלה מהר. חוקר בשם ashishkurmi מחברת StepSecurity פתח דיווח מפורט ב-19:46, כ-26 דקות אחרי הפרסום הראשון. תוך שעה ו-43 דקות כל 84 הגרסאות סומנו כמיושנות, ו-npm הסירה את הקבצים מהרישום בין 22:13 ל-23:55. תוקף שלא היה שובר את הבדיקות, כותבים המתחזקים, היה יכול לפרסם בשקט עוד שעות.

מה רץ במחשב ברגע ההתקנה

החבילות הנגועות הכילו הפניה לחבילה נוספת שיושבת על קומיט יתום ברשת ה-forks של המאגר. כשמפתח או שרת בנייה מריצים התקנה רגילה, מנהל החבילות מושך את הקומיט הזה ומריץ סקריפט שרשום להרצה אוטומטית בשלב ההכנה, וזה מפעיל קובץ מעורפל בנפח כ-2.3 מגה-בייט.

מה שהקובץ אוסף הוא רשימת המכולת של סביבת פיתוח מודרנית: פרטי אימות של AWS משירות המטא-דאטה ומ-Secrets Manager, מטא-דאטה של GCP, token-ים של חשבונות שירות ב-Kubernetes, token-ים של Vault, הקובץ ‎~/.npmrc, token-ים של GitHub ממשתני סביבה, מה-CLI ומקובץ ההרשאות המקומי, ומפתחות SSH פרטיים. את השלל הוא מוציא דרך רשת שיתוף הקבצים של מסנג'ר Session, שמוצפנת מקצה לקצה ואין מאחוריה שרת שליטה ובקרה בשליטת התוקף (C2, השרת שדרכו תוקף שולח פקודות לנוזקה), כך שהחסימה היחידה שאפשרית ברשת היא לפי הדומיינים האלה עצמם. ואז הוא מתפשט: שואל את הרישום של npm אילו חבילות אחרות מתוחזקות על ידי אותו משתמש, ומפרסם גם אותן עם אותה הזרקה.

Mistral, שנפגעה גם היא, פרסמה התרעת אבטחה משלה ובה הבחנה שכדאי לשים לב אליה. גרסאות ה-npm הנגועות של ה-SDK שלה בכלל לא עבדו, כי הסקריפט הפנה לקובץ שלא היה קיים, אבל גרסה 2.4.6 של החבילה ב-PyPI כן הריצה קוד בכל import ופתחה תהליך רקע שאוסף פרטי אימות. אותה מתקפה, שתי תוצאות שונות לגמרי.

ארבעה חודשים של שקט

כאן חוזרים ל-CrowdSec. בין 05:52:29 ל-06:01:33 UTC ב-22 במאי מישהו הוריד כ-170 מאגרים פרטיים של החברה. בתוך הארכיון שפורסם נשאר קובץ ההגדרות של git עם כתובת המקור, ובה ה-token ששימש לשכפול, מסוג OAuth.

ומכאן התחקיר נתקע. ה-token כבר לא היה קיים כשהחברה גילתה על הדליפה, והחיפוש אחרי הגרסה המוצפנת שלו ביומן הביקורת של הארגון לא העלה כלום. הסיבה טכנית ומטרידה: יומן הביקורת של GitHub מתעד רק פעולות מסוימות, ופעילות git עצמה נשמרת רק בתוכניות ארגוניות, ושם בחלון מתגלגל של שבעה ימים. CrowdSec לא בתוכנית כזאת. בפועל זה אומר ששכפול של מאגר פרטי יכול לקרות בלי שיישאר לכם ממנו זכר בלוגים.

מה שפתר את זה היה תמיכה של GitHub, שעקבה אחרי מחזור החיים המלא של ה-token והצליבה אותו לחשבון. החשבון היה של מפתח שזה עתה עזב את החברה ונשאר בארגון מסיבה לגיטימית, כי הפרידה הייתה בטוב והוא רצה לסיים עבודה פתוחה. הגישה שלו בוטלה ב-25 במאי, שלושה ימים אחרי השכפול. ביקורת על הפעילות שלו הראתה שכפול בלבד: לא קומיט, לא שינוי בקוד, לא נגיעה בתשתית או ב-CI.

מי עמד מאחורי ההורדה? CrowdSec מייחסת אותה לחשבון בשם diencracked, שהיא מקשרת לפורום BreachForum, מכתובת IP בטורונטו ובאזור זמן UTC-4. הם עצמם מסייגים: זה מה שנתוני ה-git מראים, לא יותר מזה.

מה באמת היה בפנים

השאלה האמיתית בכל דליפת קוד היא לא כמה מאגרים אלא מה היה בהם. לפי הדוח, ה-token השימושי היחיד היה מפתח AWS לשירות התראות, ומישהו אכן ניסה אותו ב-17 באוגוסט מכתובת 23.234.84.102 והריץ בדיקות זהות ורשימת ערוצים. ההרשאה שלו הייתה מוגבלת לפרסום בערוץ יחיד, והוא לא הוביל לשום מקום. התשתית ומסדי הנתונים לא נגעו, ואף שורת קוד לא שונתה.

מה שכן יצא החוצה הוא 83 כתובות מייל של משתמשים מתוך כ-150 אלף, וגם שמות, כתובות ופרטי השקעה של 51 משקיעים פוטנציאליים משנת 2020. על האחרונים המנכ"ל מתנצל בדוח אישית ומודיע שהעניין ידווח לרשויות.

מה זה אומר בפועל

הפער שהחברה מודה בו בעצמה הוא נקודת המוצא הכי שימושית כאן: באותה תקופה לא הייתה הגנת endpoint על מחשבי המפתחים, והיום היא מותקנת על כל מכונה שנוגעת בקוד או בתשתית, עם דגש על זיהוי חבילות ותוספים זדוניים. אם אתם מתקינים חבילות npm על אותה מכונה שמחזיקה מפתחות ענן ו-token-ים ל-GitHub, המכונה הזאת היא הנכס, לא הקוד. בדקו קובצי נעילה, תמונות קונטיינר ו-cache של בנייה מול רשימות הגרסאות הנגועות אם נגעתם ב-‎@tanstack/*‎ סביב 11 במאי או ב-SDK של Mistral באותם ימים. ולבסוף, נוהל עזיבה: גישה שהשארתם פתוחה מסיבה טובה היא עדיין גישה פתוחה.