קישור חד-פעמי שנמחק בבקשת GET רגילה מת ברגע ש-Slack, Teams או שער
אבטחה של אימייל טוענים אותו כדי ליצור תצוגה מקדימה. הפתרון הוא להחזיר
ב-GET רק מטא-דאטה, ולמחוק את הסוד רק בפעולת הצגה מפורשת ב-POST, עם
ביטוי סיסמה שנשלח בערוץ שני כרשת ביטחון. הפוסט מסביר אילו מערכות טוענות
קישורים בלי לשאול, ואיך לתכנן כך שאף אחת מהן לא תזיק.
מחולל הקישורים הסודיים החד-פעמיים של LinkPilot בנוי כך: טעינה לצורך תצוגה מקדימה מקבלת רק את המעטפת של הדף. הסוד נטען, והצפייה נספרת, רק כשדפדפן מריץ את הדף.
מצב הכשל הרגיל
רוב תוכנות הצ'אט והאימייל מנסות ליצור תצוגה מקדימה כשהן רואות כתובת URL. התהליך נראה כך:
- המשתמש מדביק את הקישור הסודי ב-Slack.
- הבוט של Slack טוען את הכתובת (שולח
GET; Slack מתעד את ההתנהגות הזו תחת link unfurling). - הקוד שמטפל בהצגה מתייחס לבקשה כאל צפייה של אדם, מוחק את הסוד ומחזיר את התוכן.
- Slack מקבל את התוכן שנמחק (או דף כללי) ומציג אותו.
- האדם לוחץ על הקישור, רואה "already viewed" ופונה לשולח כדי לקבל קישור חדש.
לבוט לא הייתה כוונה רעה. הקוד ערבב בין "מישהו טען את הכתובת" לבין "הנמען קרא את הסוד". אלה שני אירועים שונים, וכל בעיית התכנון היא להפריד ביניהם.
בשלב 3 מסתתרת גרסה גרועה יותר: אם בקשת ה-GET של הבוט קיבלה את התוכן
עצמו, הסוד יושב עכשיו בתשתית של צד שלישי שמייצרת תצוגות מקדימות. במקרה
הטוב הוא נמחק אחרי שהתצוגה נבנתה, אבל אין לכם דרך לוודא את זה, ואין אירוע
ביומן הביקורת שמספר לכם שזה קרה.
מי טוען קישור לפני שהנמען פותח אותו?
בוטים של תצוגה מקדימה בצ'אט, שערי אבטחה של אימייל, הגנת קצה, פרוקסי ארגוניים ותוספי תצוגה מקדימה, ובדרך כלל יותר מאחד מהם. "Slack יוצר תצוגות מקדימות" ממעיט בגודל הבעיה. כשמדביקים כתובת URL במקום עבודה מודרני, בדרך כלל כמה מערכות טוענות אותה לפני שאדם כלשהו לוחץ עליה:
| מי טוען | מתי זה קורה | אפשר לזהות? |
|---|---|---|
| בוטים של תצוגה מקדימה בצ'אט (Slack, Teams, iMessage, WhatsApp, LinkedIn) | ברגע שההודעה נשלחת | בדרך כלל: User Agent ייחודי כמו Slackbot-LinkExpanding |
| שערי אבטחה של אימייל (Mimecast, Proofpoint, Microsoft Defender) | במסירה, ולעיתים קרובות שוב בזמן הלחיצה | לעיתים קרובות לא: רבים מציגים User Agent של דפדפן אמיתי |
| הגנת קצה / אנטי-וירוס | כשהכתובת מגיעה למחשב של הנמען | לעיתים רחוקות |
| פרוקסי ארגוניים | בזמן הלחיצה, לפני שהבקשה של המשתמש מועברת הלאה | לעיתים רחוקות |
| תוספי דפדפן לתצוגה מקדימה של קישורים | במעבר עכבר או בטעינת הדף | לא: הם פועלים בתוך סשן הדפדפן של המשתמש עצמו |
ההתנהגות של הספקים בטבלה משתנה כל הזמן. התייחסו לטענות על שערים מסוימים
כ"נכון לפי מה שפורסם ביולי 2026, יש לבדוק מול התיעוד העדכני". המכנה המשותף
לא משתנה: כל המערכות האלה שולחות GET רגיל, כי זו הדרך הבטוחה והאחידה
היחידה להסתכל על כתובת שלא בשליטתכם.
איך מונעים מתצוגה מקדימה למחוק קישור חד-פעמי?
החזירו מטא-דאטה ב-GET ומחקו רק ב-POST מפורש. הסמנטיקה של HTTP כבר
קיימת: RFC 9110, סעיף 9.2.1
מגדיר את GET כבטוח ואידמפוטנטי, כלומר בקשה שכל אחד רשאי לשלוח בחופשיות
כי היא לא אמורה לשנות שום דבר. POST הוא הפועל של פעולה. לכן הפכו את
ההצגה לפעולת POST מאחורי כפתור גלוי:
GET /s/abc123 → HTML page: "Click to reveal this secret"
POST /s/abc123 → burns the secret, returns the payload
עכשיו בקשת ה-GET של Slack מחזירה דף מטא-דאטה ניטרלי. הסוד נמחק רק
כשהאדם לוחץ על הכפתור. כל סורק שמכבד את הסמנטיקה של HTTP לא נוגע בתוכן
בכלל, וזה כמעט כולם, כי שליחת בקשות POST עם תופעות לוואי לכתובות
אקראיות הייתה שוברת חצי מהאינטרנט.
דוגמה מלאה, צעד אחר צעד
עקבו אחרי קישור אחד מתחילתו ועד סופו. אתם יוצרים סוד עם מחיקה לאחר קריאה וביטוי סיסמה, ואז מדביקים את הכתובת בהודעה פרטית ב-Slack ובאימייל.
- הבוט של Slack שולח
GET /s/abc123. התגובה: דף מטא-דאטה קטן, עם הסטטוס "active" (פעיל) ודרישה לביטוי סיסמה. לא נרשמת צפייה. הסוד עדיין קיים. - שער האימייל של הנמען משכתב את הכתובת באימייל וטוען אותה בזמן המסירה
לניתוח מוניטין. אותו
GET, אותו מטא-דאטה. הסוד עדיין קיים. - הנמען לוחץ על הקישור באימייל. הסורק של השער בזמן הלחיצה טוען אותו
שוב (
GET) ואז מפנה את הדפדפן הלאה. - הדפדפן טוען את דף ההצגה, עוד
GET. הדף מציג כפתור הצגה ושדה לביטוי הסיסמה. עדיין לא נרשמת צפייה. - הנמען מזין את ביטוי הסיסמה ולוחץ על כפתור ההצגה. הדפדפן שולח
POST /s/abc123. הצפייה נרשמת, התוכן מוחזר והסוד נמחק. - כל מי שטוען את הכתובת אחר כך מקבל תגובה מפורשת שהסוד "כבר נצפה והושמד", במקום התוכן.
חמש טעינות, הצגה אחת. היחס הזה רגיל בסביבה ארגונית. כל תכנון שלא עומד בו ייכשל כבר ביום הראשון, והשולח פשוט ידביק את הסיסמה בצ'אט במקום.
הגנה בשכבות
ההפרדה בין GET ל-POST
מטפלת בכל בוט תצוגה מקדימה וסורק שמתנהגים כראוי, וזה הרוב המכריע של התעבורה האוטומטית. זה הבסיס. כל מה שמופיע בהמשך הוא שכבות נוספות.
שכבת ביטוי סיסמה
מכסה את מה שההפרדה לא מכסה: סביבות sandbox שמריצות JavaScript ומבצעות פעולות בדף, וגם לחיצות אנושיות בטעות ואימיילים שהועברו הלאה. sandbox שמפעיל קישורים חשודים יכול ללחוץ על כפתור הצגה, אבל הוא לא יכול להזין ביטוי סיסמה שנשלח בערוץ אחר. שלחו את הקישור באימייל ואת ביטוי הסיסמה בהודעה פרטית ב-Slack או ב-SMS.
סינון בוטים לפי User Agent
חסמו בוטים מוכרים של תצוגה מקדימה (Slackbot, LinkedInBot, Twitterbot)
לפני שהם מגיעים ללוגיקת ההצגה. כדאי לעשות את זה כאמצעי נוסף, אבל זה אף
פעם לא מספיק לבד: סורקי אבטחה מציגים באופן קבוע User Agent של דפדפן אמיתי,
בדיוק כדי שדפים זדוניים לא יוכלו להסתתר מפני הניתוח.
תפוגה קצרה
הגבילו את חלון הנזק. קישור שחי רבע שעה משאיר אצל הסורקים, בקובצי הלוג ובהיסטוריית הצ'אט כתובת שכבר לא מובילה לשום דבר.
Cache-Control: no-store
הגדירו אותו בכל תגובה של הצגה, כדי שאף גורם ביניים, לא פרוקסי ארגוני, לא CDN ולא מטמון הדפדפן, ישמור עותק של התוכן אחרי הקריאה הלגיטימית היחידה.
שערי אבטחה של אימייל במיוחד
שערים ארגוניים מקבלים פרק משלהם כי הם טוענים את הקישור פעמיים. מוצרים מהסוג הזה (Microsoft Defender Safe Links הוא הדוגמה המתועדת) משכתבים כל כתובת בדואר נכנס כך שתעבור דרך דומיין הסריקה שלהם. בדרך כלל הכתובת נטענת פעם אחת בזמן המסירה לניתוח מוניטין, ושוב בזמן הלחיצה, לפני שהאדם מופנה ליעד האמיתי.
אם אחת הטעינות האלה הייתה מוחקת את הסוד, הנמען היה רואה תמיד קישור
מת. למשתמשים זה נראה כמו כשל אקראי, אבל הוא דטרמיניסטי לגמרי. ההפרדה בין
GET ל-POST סופגת את שתי הטעינות בלי שום עלות, כי שתיהן GET רגיל.
ההפניה בזמן הלחיצה מביאה את האדם לדף ההצגה, ושם מתבצע ה-POST.
מקרי קצה שעדיין כואבים
- סביבות sandbox שמריצות JavaScript. חלק מהסביבות שמפעילות קישורים
חשודים מציגות את הדף ומבצעות בו פעולות. sandbox אגרסיבי יכול, באופן עקרוני,
ללחוץ על כפתור ששולח
POSTאם הדף לא דורש ביטוי סיסמה. שכבת ביטוי הסיסמה היא רשת הביטחון. - הקישור וביטוי הסיסמה באותו ערוץ. אם שניהם נשלחים באותו אימייל, כל ההגנות שלמעלה מצטמצמות ל"נקווה שאף אחד לא יקרא את האימייל הזה". תמיד הפרידו בין הערוצים.
- העברה הלאה. כל העברה לצ'אט או למערכת דואר חדשה מפעילה סבב חדש של תצוגות מקדימות. עם ההפרדה זה לא מזיק, אבל כל תחנה היא עוד סט של לוגים שמחזיק את הכתובת, וזו סיבה נוספת לתפוגה קצרה.
- הצגות בו-זמניות. אם שני אנשים לוחצים על כפתור ההצגה באותו רגע, הקריאה והמחיקה חייבות לקרות בצעד אחד שאי אפשר לפצל: בקשה אחת בדיוק מנצחת, והשנייה מקבלת "already viewed". מימוש שבו הקריאה והמחיקה הן שני צעדים נפרדים עלול להחזיר את התוכן פעמיים.
בדקו את הכלי שלכם
לפני שאתם מפקידים פרטי התחברות אמיתיים בידי שירות לשיתוף מידע רגיש, הריצו את הבדיקה הזו, שלוקחת שתי דקות:
- צרו סוד לבדיקה עם מחיקה לאחר קריאה.
- טענו אותו מהטרמינל. זה בדיוק מה שבוט תצוגה מקדימה עושה:
curl -s -o /dev/null -w "%{http_code}" "<url>" - פתחו את הקישור בדפדפן. אם מופיע "already viewed", הכלי מוחק את הסוד
ב-
GETולא בטוח להדביק את הקישור בצ'אט או באימייל. - אם הסוד שרד, הציגו אותו כרגיל בדפדפן ואז הריצו
curlשוב. עכשיו אתם אמורים לקבל תגובה מפורשת שהסוד נמחק, ולא את התוכן.
איך LinkPilot מממש את זה
לפרוטוקול, זה התכנון שהקישורים הסודיים של LinkPilot משתמשים בו: בקשת GET
לנקודת הקצה של ההצגה מחזירה רק מטא-דאטה (סטטוס, והאם נדרש ביטוי סיסמה),
וצפייה נרשמת רק בהצגה מפורשת ב-POST. ביטויי הסיסמה עוברים hash ב-SHA-256
בצד הלקוח, ויש מגבלה של חמישה ניסיונות לכל IP לדקה לכל סוד. כל תגובת
הצגה נושאת Cache-Control: no-store, סודות שנמחקו או שפג תוקפם מחזירים
HTTP 410 והתוכן השמור נדרס, ויומן הביקורת רושם כל אירוע עם IP שעבר hash
עם salt שמתחלף כל יום. כתובת ה-IP הגולמית אף פעם לא נשמרת. כל הפרטים
בדף ארכיטקטורת האבטחה.
נסו בחינם, בלי הרשמה
הודעה שמשמידה את עצמה
שלחו פתק שנעלם אחרי קריאה אחת.
המגבלות של התכנון הזה
הגבולות, בכנות: שום דבר מכל זה לא מגן על סוד אחרי הצגה לגיטימית. הנמען יכול להעתיק את התוכן, לצלם אותו או להעביר אותו הלאה. זה לא עוזר אם ביטוי הסיסמה נשלח יחד עם הקישור. וזה לא יכול למנוע מאדם שקיבל את הקישור בטעות לפתוח אותו לפני הנמען המיועד. בשביל זה יש יומן ביקורת: אתם רואים את הצפייה הלא צפויה ומחליפים את פרטי ההתחברות.
לקריאה נוספת
מקורות
- IETF, RFC 9110: HTTP Semantics, section 9.2.1 Safe Methods.
- Slack API, Unfurling links in messages.
- Microsoft Learn, Safe Links in Microsoft Defender for Office 365.
