2013-07-20

אבני הבנין של האינטרנט: SOP ותקשורת Cross-Domain


כלל ה Same Origin Policy, או בקיצור SOP, הופיע לראשונה בדפדפן Netscape 2.0. הוא הופיע בד-בבד עם שפת התכנות JavaScript וה (Document Object Model (DOM. כמו JavaScript וה DOM, הוא הפך לחלק בלתי-נפרד מהדפדפן המודרני.

SOP הוא עקרון האבטחה, כנראה המרכזי ביותר בדפדפן. הרעיון הוא דיי פשוט: שני Execution Contexts שונים של ג'אווהסקריפט לא יוכלו לגשת על ה DOM אחד של השני אם הם לא מגיעים מאותו המקור (origin).
מתי יש לנו Execution Contexts שונים? כאשר יש iFrames שונים או טאבים שונים.

איך מוגדר origin? שילוב של שלושה אלמנטים:
  • סכמה / פרוטוקול (למשל http או https)
  • Fully Qualified Domain Name (בקיצור: FQDN, למשל news.google.co.il)
  • מספר port [א].

SOP מונע מצב בו פתחנו 2 חלונות: אחד לאתר לגיטימי והשני לאתר זדוני (בטעות), והאתר הזדוני עושה באתר הלגיטימי כרצונו. בנוסף יש לנו כלי בשם iFrame - כלי ל isolation, כך שאנו יכולים לפתוח בדף שלנו iFrame לאתר חיצוני לא מוכר ולדעת שאנו מוגנים בפניו.

עולם האינטרנט השתנה מאז 1995, והיום יש הרבה יותר אינטראקציה משותפת (mashups) בין אתרים שונים. SOP מגביל את היכולת שלנו לשתף מידע בין מקורות שונים.


אז מה עושים? האם נחרץ גורלו של האתר שלנו להיות מוגן, אך מבודד - לעד?

בפוסט זה נבין מה מדיניות ה SOP בדיוק אומרת, ונסקור מספר טכניקות נפוצות לבצע שיתוף מידע.


פוסט זה שייך לסדרה אבני הבניין של האינטרנט.






תזכירו לי מה המשמעות של SOP?


נניח שדף ה HTML של האפליקציה / אתר שלנו נטען מהכתובת http://www.example.com/home/index.html.
משמע: ה origin הנוכחי שלנו הוא http://www.example.com:80.


הנה המשמעות של מדיניות ה SOP בנוגע לגישה ל origins אחרים ("Compared URL"):

מקור: http://en.wikipedia.org/wiki/Same_origin_policy

הערה מעניינת: SOP, כמו מנגנוני הגנה אחרים של הדפדפן, מתבססים על ה FQDN ללא אימות מול ה IP Address. המשמעות היא שפריצה לשרת DNS יכולה להשבית את ה SOP ולאפשר קשת רחבה ויצירתית של התקפות על המשתמש.


SOP הוגדר במקור עבור גישה ל DOM בלבד, אך עם השנים הוא הורחב:

XMLHttpRequest (המשמש לקריאות Ajax) מוגבל גם הוא. קריאות Ajax יוגבלו רק ל origin ממנו נטען המסמך (כלומר: קובץ ה HTML). בנוסף, בעקבות היסטוריה של התקפות שהתבססו על "זיוף" קריאות ל origin הלגיטימי עם Headers מטעים - נוספו מספר מגבלות על Headers אותם ניתן לשנות בקריאות Ajax (למשל: Referer או Content-Length וכו').

Local Storage (יכולת חדשה ב HTML5 לשמור נתונים באופן קבוע על הדפדפן) גם היא מוגבלת ע"פ ה SOP. לכל origin יש storage שלו ולא ניתן לגשת ל storage של origin אחר.

Cookies - האמת שמגבלות על Cookies החלו במקביל להתפתחות ה SOP. התוצאה: סט מגבלות דומה, אך מעט שונה. הדפדפן לא ישלח Cookies לדומיין אחר (למשל evil.com) אך הוא ישלח את ה cookies לתת-domain למשל:
evil.www.example.com, תת-domain של www.example.com.
בדפדפנים שאינם Internet Explorer, ניתן לדרוש אכיפה של domain מדויק ע"י השמטה של פרמטר ה domain (תיעוד ב MDN, קראו את התיאור של הפרמטר domain).

כיוון ש Cookie יכולים להכיל מידע רגיש מבחינת אבטחת מידע (למשל: אישור גישה לשרת), הוסיפו עליהם עוד 2 מנגנוני הגנה נוספים:
httponly - פרמטר ב HTTP header של set-cookie שהשרת יכול לסמן, המונע גישה מקוד ג'אווהסקריפט בצד-הלקוח ל cookie שטמן השרת. כלומר: ה cookie רק יעבור הלוך וחזור בין השרת לדפדפן, בלי שלקוד הג'אווהסקריפט תהיה גישה אליו.
secure - פרמטר בצד הג'אווהסקריפט של יצירת cookie (שוב, התיעוד ב MDN) שאם נקבע ל true - יגרום לדפדפן להעביר את ה cookie רק על גבי תקשורת HTTPS (כלומר: מוצפנת).

Java Applet, Flash ו Silverlight כוללים כללים שונים ומשונים הנוגעים ל SOP. חבורה זו היא זן נכחד - ולכן אני מדלג על הדיון בעניינה.


SOP מתיר חופש במקרים הבאים:

Cross domain resource loading - שימו לב, זהו כלל חשוב: הדפדפן כן מאפשר לטעון קבצים מ domains אחרים. לדוגמה: קבצי ג'אווהסקריפט, תמונות, פונטים (בעזרת font-face@) או קבצי וידאו. על קבצי CSS יש כמה מגבלות [ב]. כלומר: האתר שלנו, www.example.com יכול בלי בעיה לטעון קובץ ג'אווהסקריפט מאתר אחר suspicious.com. טעינת הג'אווהסקריפט הינה הצהרת אמון במקור - ועל כן קוד הג'אווהסקריפט שנטען מקבל את ה origin שלנו ועל כן הוא יכול לגשת ל DOM שלנו ללא מגבלה. מצד שני: הוא אינו יכול לגשת ל DOM של iFrame אחר שמקורו מ suspicious.com או לבצע קריאות Ajax ל suspicious.com - למרות שהוא נטען משם.

הערה: אשתמש מעתה לסירוגין במונחים "frame" ו "iFrame", ע"פ הביטוי הספציפי וכיצד שנהוג להשתמש בו. לצורך הדיון בפוסט זה, הכוונה היא לאותו הדבר.

Location - סקריפט שמגיע מ origin שונה מורשה לבצע פעולות השמה (אך לא קריאה) על אובייקט ה Location (ה URL הנוכחי של ה frame) כגון ()location.replace. זה נשמע מעט מוזר, אך הסיבה לכך היא לאפשר שימוש בטכניקה בשם iFrame busting הנועדה להילחם בהתקפה בשם clickjacking. כלומר: כדי להבטיח שאתר זדוני לא מארח את האתר שלנו ומראה רק חלקים מסוימים ממנו כחלק מהונאה, מותר לנו לגשת לכל frame אחר בדף (למשל ה Top Frame) ולהפוך אותו לכתובת האתר שלנו. התוצאה: האתר המארח יוחלף באתר שלנו - ללא אירוח.
דרך מודרנית יותר למניעת clickjacking היא שימוש ב HTTP Header בשם X-Frame-Options.


מתקפת clickjacking בפעולה. מקור.


באופן לא מפתיע יש הרבה יוצאי דופן בין הדפדפנים השונים. מקור עיון מעולה לנושא זה הוא מדריך האבטחה של כרום.



טכניקות התמודדות

עם השנים התבססו מספר טכניקות מוכרות "לעקוף" את מגבלות ה SOP, במצבים בהם אנו מעוניינים בכך. חלק מהיכולות אופשרו ע"י הדפדפנים, ואחרים ניצלו "פרצות" בחוקי ה SOP בכדי לבצע את העבודה.

רשימת הטכניקות אותן נסקור בפוסט זה

בגדול ניתן לחלק את הטכניקות ל2 משפחות:
  1. תקשורת בין Frames מ Origins שונים באותו הדף.
  2. תקשורת בין לקוח לשרת ב Origin שונה (כלומר: קריאות Ajax).
אני אפתח במשפחה הראשונה, למרות שהשימוש בה פחות נפוץ בימנו - מכיוון שחלק מהטכניקות שלה מהוות בסיס לטכניקות במשפחה השנייה.






Cross-Origin inter-Frame Communication





להלן נסקור מספר טכניקות מקובלות כדי לתקשר בין iFrames מ origins (או domains) שונים.


Domain Relaxation - הטכניקה הזו אופשרה ע"י הדפדפנים.
בטכניקה זו קוד הג'אווהסקריפט משנה את ה origin הנוכחי ע"י השמת ערך חדש למשתנה: document.domain.
המגבלה: הוא יכול רק "לקצץ" תתי-domains ולא להחליף את ה domain לגמרי. לדוגמה:
  • מ "login.example.com" ל "example.com" - מותר.
  • מ "login.example.com" ל "login.com" - אסור.
  • כמו כן, מ "login.example.com" ל "com" - מותר, אבל מסוכן!!
התוצאה של פעולת ה domain relaxation היא הבעת אמון גדולה בכל אתר מה domain המעודכן, והרשאה לג'אווהסקריפט של אותו האתר לערוך את ה DOM שלנו. אם אפליקציה זדונית שאנו מארחים ב iFrame, מסוגלת לבצע Domain Relaxation לאותו Relaxed Domain כמו שלנו - היא יכולה לגשת ל DOM שלנו ללא מגבלה.

חשוב לציין ש Domain Relaxation לא משפיע על אובייקט ה XmlHttpRequest. קריאות Ajax יתאפשרו רק ל origin המקורי ממנו נטען ה Document שלנו.

בנוסף, בחלק מהדפדפנים Domain Relaxation ישפיע רק על גישה בין Frames באותו הטאב ולא על גישה בין טאבים נפרדים.

סיכום: Domain Relaxation היא טכניקה נוחה בתוך ארגון בו כל המחשבים הם תחת דומיין-על אחיד, אך היא לא נחשבת לבטוחה במיוחד. שימוש ב Domain Relaxation פותח פתח למרחב לא ידוע של תתי-domains שאנו לא מכירים - לגשת ל DOM שלנו.


Encoding Messaged on the URL Fragment Id

טריק מלוכלך זה מתבסס על 2 עובדות:
  • SOP מתיר ל Frame אחד לשנות את ה Location (כלומר URL) של Frame כלשהו אחר בדף.
  • שינוי של FragmentID (החלק ב URL שלאחר סימן ה #) לא גורם ל reload של המסמך (כפי שהסברנו בפוסט על ה URL)
התרגיל עובד כך: פריים (frame) א' משנה את ה FID (קיצור של Fragment ID) של פריים ב'. הוא מקודד הודעה כלשהי שהוא רוצה להעביר.
פריים ב' מאזין לשינויים ב FID שלו, מפענח את ההודעה ומחזיר תשובה ע"י קידוד הודעה ע"י שינוי ה FID של פריים ב'.

וריאציה נוספת של הטכניקה הזו היא שכל פריים משנה את ה window.name של עצמו. SOP מתיר ל frames שונים לקרוא את ה property הזה מ frames אחרים ללא הגבלה.

סיכום: מלוכלך, מוגבל, לא בטוח (אני לא ידוע מי באמת שינה לי את ה FID) - אבל יכול לעבוד.
ישנן עוד כמה טכניקות דומות (בעזרת Flash, למשל) - אך אני אדלג עליהן.


Post Message


בשלב מסוים החליטו הדפדפנים לשים סוף לבאלגן. כשהרבה מפתחים משתמשים בטכניקות כגון ה Encoding על ה FID, עדיף פשוט לאפשר להם דרך פשוטה ובטוחה. כאן נכנסת לתמונה יכולת ה "Post Message", שהוצגה כחלק מ HTML5.

יכולת זו נתמכת החל מ IE8 (כולל), ועל כן היא רלוונטית עבור הרוב הגדול של הדפדפנים שבשימוש היום.

מנגנון ה Post Message (בקיצור: PM) מאפשר לשלוח הודעות טקסט בין iFrames או חלונות שונים בדפדפן, בתוספת מנגנון אבטחה שמגביל את שליחת / קבלת ההודעות ל domain ידוע מראש.

הנה דוגמה:


כדאי לציין שניתן לשלוח הודעה עם target domain של "*" ("לכל מען דבעי"). לא ברור לי מדוע איפשרו יכולת זו - אבל היא מדלגת על מנגנון האבטחה החשוב של PM. כמו כן חשוב לבצע בדיקת domain בקבלה ולבדוק רק Domain מדויק.
מימוש אפשרי הוא בדיקה חלקית, למשל:

if (msg.origin.indexOf(".example.com") != -1) { ... }

מזהים מה הבעיה פה?
תוקף מ Domain בשם example.com.dr-evil.com (השייך לחלוטין לדומיין dr-evil.com) יוכל גם הוא לשלוח לנו הודעות!

סיכום: טכניקה בטוחה וקלה לשימוש. כדאי תמיד להעדיף אותה - אם היא זמינה.

שווה לציין ספריה בשם EasyXDM לשליחת הודעות Cross Domain.
EasyXDM ישתמש ב Post Message, אם הוא זמין (IE8+) או יבצע fallback למגוון שיטות היכולות לעבוד על IE6 ו IE7 - אם אין לו ברירה. מומלץ להשתמש רק אם תמיכה ב IE6-7 היא חובה.






Cross-Origin Client-To-Server Communication




סקרנו את המשפחה הראשונה של הטכניקות: תקשורת בין Frames בדפדפן. טכניקות אלו שימושית כאשר אנו מארחים אפליקציות או widgets ב iFrame.

הצורך הנפוץ יותר הוא בביצוע קריאות לשרת שנמצא ב origin שונה. תזכורת: אם האפליקציה שלנו נטענה מ origin A והיא תנסה לבצע קריאת Ajax ל origin B - הקריאה לא תצא, הדפדפן יחסום אותה.

בואו נעבור על מספר טכניקות מקובלות המאפשרות לבצע קריאת Ajax ל originB - וננסה לעשות זאת בצורה מאובטחת ככל האפשר.


Server Proxy

כאשר יש לנו דף / document המשויך ל Domain A, אין לו בעיה לייצר קריאות Ajax ל Domain A - הרי זה ה origin שלו. אם אנו מנסים להוציא קריאת Ajax ל Domain B (כלומר: domain אחר), הדפדפן יחסום את הקריאה כחלק ממדיניות ה SOP:



אפשרות אחת להתמודד עם הבעיה היא לבקש מהשרת ב Domain A, לשמש עבורנו כ "Proxy" ולבצע עבורנו את הקריאה:



לשרת אין מדיניות SOP, ולכן אין לו מגבלה לתקשר מול שרת אחר שנמצא ב Domain B. אין משמעות הדבר שאין סיכון בתקשורת לשרת אחר - פשוט אין אכיפה. על שרת A לממש את מנגנוני האבטחה בעצמו ולהחליט באילו שרתים אחרים הוא בוטח.

וריאציה אחרת של שיטה זו היא להציב Reverse Proxy בין הדפדפן ל 2 השרתים. ניתן לקבוע חוקים ב Reverse Proxy שייגרמו ל-2 השרתים להראות כאילו הם באותו ה Domain.

גישת ה Server Proxy היא פשוטה, אולם יש לה כמה חסרונות:
  • השרת צריך לבצע עבודה נוספת של העברת הודעות בין הדפדפן לשרת ב' , מה שיכול בהחלט לפגוע לו ב Scalability (עוד חומרה = עוד כסף). במקרה של Reverse Proxy - העלות הנוספת היא ברורה יותר.
  • שרת ה Proxy שלנו הוא לא דפדפן, הוא לא יעביר באופן טבעי User Agent או Cookies, אלא אם נוסיף קוד שיעשה זאת.
  • "הערמנו" על הדפדפן ועל מנגנון האבטחה שלו, אבל האם יצרנו אלטרנטיבה בטוחה מספיק? (האם יצרנו בכלל אלטרנטיבה בטוחה במידה כלשהי?)
סיכום: פתרון פשוט אבל בעייתי: יש לשים לב למחיר הנוסף ב Scalability, ולוודא שלא יצרנו פרצת-אבטחה.


JSONP (קיצור של JSON with Padding)

טכניקת ה JSONP מבוססת על העובדה הבאה:
  • SOP לא מגביל אותנו לטעון קבצי JavaScript מ Domains אחרים.

מה היה קורה אם קוד הג'אווהסקריפט, שנטען מ Domain אחר, היה כולל קוד שיוצר במיוחד עבור הקריאה שלנו? למשל, קוד המבצע השמה של שורת נתונים לתוך משתנה גלובלי שאנו יכולים לגשת אליו? - משמע שהצלחנו להעביר נתונים, Cross-domain, לדפדפן!

הנה דוגמת קוד כיצד אנו קוראים מצד-הלקוח ל API מסוג JSONP:


והנה התשובה שהשרת מייצר (דינמית) = הקובץ info.js:


הערך "jsonpCallBack" הגיע כפרטמר על ה URL של ה Request, ושאר הנתונים הגיעו מהשרת (מבנה נתונים, DB וכו').
הקוד הנוסף שעוטף את ה data, בין אם זו השמה למשתנה גלובלי או קריאה ל callback ששמו נשלח - נקרא "Padding". זהו ה P בשם JSONP.
לרוב אנו נעדיף Padding מסוג callback, מכיוון ש callback מייצר trigger ברגע המידע חזר מהשרת. כאשר משתמשים ב Padding מסוג "השמה למשתנה גלובלי" אנו נאלץ לדגום את המשתנה שוב ושוב בכדי לדעת מתי הוא השתנה...

ל JSONP יש מספר חסרונות:
  • נדרשת תמיכה מהשרת: על השרת לייצר Padding מתאים לנתונים. ה Padding חייב לקרות בצד השרת ואין דרך "להשלים" אותו בצד הלקוח עבור קובץ ג'אווהסקריפט שלא כולל אותו.
  • מכיוון שלא ניתן לטעון קובץ ג'אווהסקריפט יותר מפעם אחת, אם אנו רוצים לבצע מספר קריאות JSONP יהיה עלינו לייצר שם חדש לקובץ ה script בכל קריאה = מעמסה.
  • JSNOP מוגבל לפעולות HTTP GET בלבד (לא ניתן לטעון scripts במתודת HTTP אחרת) - עובדה שמונעת מאתנו להשתמש ב JSONP כדי לקרוא ל REST API.
  • אין Error Handling. אם קובץ הג'אווהסקריפט לא נטען (404, 500 וכו') - אין לנו שום דרך לדעת זאת. פשוט לא יקרה שום דבר.
  • אבטחת מידע: השרת ממנו אני טוען את הנתונים ב JSONP לא מעביר רק נתונים - הוא מעביר קוד להרצה. אני צריך לסמוך על השרת הזה שלא ישלח לי קוד זדוני.

יתרון משמעותי של JSONP הוא הפשטות (היחסית) כאשר יש לנו קריאה בודדת והעובדה שהוא יעבוד גם עם דפדפנים ישנים. ניתן למצוא הרבה WEB APIs של חברות חשובות כגון גוגל או טוויטר - החשופים ב JSONP.

סיכום: אופציה סבירה כאשר יש תקשורת מועטה ואנו סומכים על שרת היעד.


iFrame Proxy / Relay
טכניקת ה iFrame Proxy (או iFrame Relay) מבוססת על תקשורת בין iFrames, וספציפית על Post Messages.
המנגנון עובד כך:
  1. פתח iFrame שה URL שלו מצביע עד דף (שהכנו מראש) הנמצא על הדומיין איתו אנו רוצים לתקשר. ה origin של המסמך ב iFrame יהיה אותו ה domain.
  2. הדף הנ"ל יטען קובץ ג'אווהסקריפט (שהכנו מראש), נקרא לו proxy.js.
  3. נבצע קריאות Post-Message ל iFrame שייצרנו. proxy.js יאזין לקריאות אלו, כאשר מנגנון ה Post-Message מספק לנו אבטחה.
  4. proxy.js יבצע קריאת Ajax לדומיין המרוחק, אין לו מגבלות - כי הדומיין הזה הוא ה origin שלו.
אם השרת מחזיר תשובה, היא תגיע ל Proxy.js.
  1. proxy.js מקבל את התשובה מהשרת ומבצע Post-Message בחרזה ל Frame / לקוד שלנו.
לגישת ה iFrame Proxy יש כמה חסרונות:
  • היא מורכבת למימוש.
  • נדרשת תמיכה מצד השרת.
  • היא דורשת תמיכה ב PM, קרי IE8+.

מצד שני היא טובה מבחינת Security:
  • בעזרת מנגנון ה PM אנו מוודאים ש proxy.js מגיע מה domain הרצוי.
  • proxy.js מבודד בתוך iFrame ואינו יכול להשפיע על הקוד שלנו - כך שלא חייבים לסמוך ב 100% על שרת היעד.

סיכום: אופציה מורכבת למימוש - אך טובה מבחינת Security.



CORS (קיצור של Cross Origin Resource Sharing)
מאחר ו JSONP ו iFrame Proxy הם אלתורים, החליטו הדפדפנים לפתור את בעיית הקריאה לשרת cross-domain בצורה שיטתית. התוצאה היא פרוטוקול ה CORS.

CORS מאפשר לנו בקלות יחסית לבצע קריאת "Ajax" לשרת בדומיין אחר. השרת צריך לממש מצדו את הפרוטוקול (כמה כללי התנהגות ו Headers על גבי HTTP).

דרך הפעולה הפנימית של CORS היא מעט מורכבת - ואינני רוצה לפרט אותה. ניתן למצוא בלינק הבא הסבר על דרך העבודה, כיצד לממש צד-שרת וכיצד לממש צד-לקוח עם jQuery.

הבעיה העיקרית של CORS היא בעיה של תמיכה. מייקרוסופט התנגדה לתקן ה CORS בטענה שהוא מורכב מדי לשימוש. ב IE8 הם שחררו מנגנון דומה בשם XCrossDomain. באופן דיי מפתיע, נראה שמייקרוסופט צדקה והמנגנון שלה פשוט יותר ובטוח יותר. אולם, הקונצנזוס בקרב שאר יצרני הדפדפנים הכריע - ו CORS הפך לתקן דה-פאקטו.

גרסת ה IE הראשונה שממשה את CORS היא IE10 (וגם לו יש באג משמעותי - הוא לא שולח cookies כמו שצריך).
בפועל התמיכה ב CORS מתחילה מ IE10 אם אינכם זקוקים ל cookies, ואם אתם זקוקים ל Cookies - היא מתחילה ב IE11. סוג האפליקציות שיכולות להסתדר עם תנאי סף שכאלו הם בעיקר אפליקציות מובייל.

עדיין אפשר לבצע מימוש יותר מורכב שישתמש ב XDomainRequest ב IE8-10 וב CORS בכל שאר המקרים.

חסרונות סה"כ:
  • ה API של CORS מעט מסורבל, מימוש בצד השרת דורש מעט עבודה.
  • CORS עשוי להזדקק ל 2 HTTP Requests (מה שנקרא "preflight") בכדי להשלים קריאה בודדת. אלו הדקויות של המימוש הפנימי.
  • התמיכה של IE היא בעייתית.
ל CORS יש גם יתרונות:
  • סטנדרטי
  • אבטחה טובה
  • לא מצריך להמציא מנגנון שלם, כגון iFrame Proxy.

סיכום: אופציה טובה, שתהיה טובה יותר בעוד מספר שנים.



סיכום

למדנו על מנגנון ה Same Origin Policy, מנגנון אבטחה מרכזי בדפדפנים המגן עלינו מפני התקפות רבות, אך משפיע רבות על היכולת של האפליקציה / אתר שלנו לתקשר עם העולם.

סקרנו והשוונו את הטכניקות הנפוצות לביצוע תקשורת Cross-Domain תחת מנגנון ה SOP, טכניקות מ-2 משפחות:
  • תקשורת בין iFrame ל iFrame.
  • תקשורת מול שרת ב Domain אחר.
סה"כ, הבנת נושא ה SOP היא חשובה למדי עבור מערכות המתקשרות בין Domains שונים, צורך ההולך וגובר עם השנים.


שיהיה בהצלחה!




----

[א] דפדפן IE לא מחשיב את ה port כחלק מה origin עבור גישה ל DOM. בפועל נדירים מ המקרים בהם דף HTML ייטען מ port לא סטנדרטי.

[ב] המגבלות שונות מדפדפן לדפדפן: IE, פיירפוקס, כרום, ספארי (יש לגלול ל CVE-2010-0051) ואופרה (לפני השימוש ב blink). אינני מתחייב שהרשימה מלאה ו/או מעודכנת.




תגובה 1:

  1. אנונימי26/8/13 23:53

    סקירה מעולה ומשובחת. פשוט סוף סוף התחברו לי כל הקצוות הפתוחים והבנתי מי נגד מה ומתי בדיוק.

    תודה, יעקב

    השבמחק