אבטחה

    איך עובדים קישורים סודיים חד-פעמיים

    קישור סודי חד-פעמי מציג את התוכן פעם אחת: הדפדפן מצפין אותו, השרת שומר רק טקסט מוצפן, וההצגה הראשונה מוחקת את העותק השמור.

    W. Miller · עודכן · על LinkPilot

    קישור סודי חד-פעמי הוא URL שמציג את התוכן שלו פעם אחת בדיוק, ואחר כך מפסיק לעבוד לצמיתות. הדפדפן של השולח מצפין את הסוד ב-AES-GCM-256, השרת שומר רק את הטקסט המוצפן, והמפתח עובר בחלק של הקישור שאחרי #k=, שדפדפנים אף פעם לא שולחים. ההצגה הראשונה אצל הנמען מפענחת את הסוד, ובאותה פעולה העותק השמור נמחק.

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

    המנגנון פשוט, אבל מקרי הקצה הם החלק המעניין, ושם מימושים רשלניים דולפים בלי שאיש ישים לב.

    התהליך הבסיסי

    1. השולח מדביק את הסוד. הדפדפן יוצר מפתח אקראי, מצפין את התוכן ב-AES-GCM-256 דרך ה-Web Crypto API, ושולח רק את הטקסט המוצפן. השרת שומר אותו תחת סיומת קישור אקראית שקשה לנחש ומחזיר URL. הדפדפן מוסיף את המפתח כחלק #k= שאחרי הסימן #, והחלק הזה לא יוצא מהמכשיר של השולח.
    2. השולח משתף את ה-URL בכל ערוץ: אימייל, צ'אט, SMS.
    3. הנמען פותח את ה-URL. בקשת GET רגילה מחזירה רק סטטוס. כשהנמען לוחץ על הצגה, השרת מסמן את הסוד כנצרך ומחזיר את הטקסט המוצפן בפעולה אחת שאי אפשר לפצל. הדפדפן של הנמען מפענח אותו עם המפתח מהחלק שאחרי הסימן #.
    4. כל ביקור נוסף מקבל עמוד סטטוס של "נמחק" במקום התוכן.

    השלב הקריטי הוא שלב 3. אם הקריאה ועדכון הסטטוס מתבצעים בשתי שאילתות נפרדות, SELECT לבדיקה ואחריה UPDATE לסימון, שני צופים בו-זמניים יכולים לעבור את הבדיקה לפני שאחד מהם כותב. שניהם מקבלים את התוכן. זה באג TOCTOU קלאסי (time-of-check to time-of-use), והוא הופך את ההבטחה "בדיוק פעם אחת" ל"בדרך כלל פעם אחת".

    למה המחיקה והקריאה חייבות לקרות בצעד אחד?

    כי אחרת שני צופים בו-זמניים יכולים לעבור את הבדיקה לפני שאחד מהם מסמן את הסוד כנצרך. הפתרון הוא לאחד את הבדיקה, הסימון והקריאה לפעולת מסד נתונים אחת. ב-Postgres, עם UPDATE ... RETURNING, זה נראה כך:

    UPDATE secrets
    SET burned_at = now()
    WHERE slug = $1
      AND burned_at IS NULL
      AND expires_at > now()
    RETURNING payload;
    

    ה-UPDATE נועל את השורה, ולכן קריאות בו-זמניות מתבצעות בזו אחר זו: הראשונה עומדת בתנאי WHERE, משנה את burned_at ומקבלת את התוכן בחזרה דרך RETURNING. כל קריאה מאוחרת יותר מוצאת ש-burned_at כבר מוגדר, לא תואמת אף שורה ומקבלת את סטטוס המחיקה. אין חלון זמן בין "בדיקה" ל"צריכה", כי שתיהן אותה פקודה.

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

    כי 410 Gone אומר שהמשאב היה קיים ואינו זמין עוד לצמיתות, וזה בדיוק המצב של סוד שנמחק. אחרי שהקישור נצרך, הוא צריך להיכשל באופן ברור וקבוע. LinkPilot עושה שני דברים במחיקה:

    • התוכן השמור נדרס ב-[REDACTED] באותה פעולת כתיבה שמשנה את הסטטוס, כך שגם פריצה עתידית למסד הנתונים לא תוכל לשחזר סוד שכבר נצרך.
    • ניסיונות הצגה נוספים מחזירים HTTP 410 Gone עם עמוד ברור שאומר שהתוכן "כבר נצפה והושמד".

    410 הוא קוד הסטטוס הנכון: הוא אומר שהמשאב היה קיים ושהוא לא זמין עוד, בכוונה ולצמיתות. 404, לעומתו, אומר "אין לנו מושג על מה מדובר". ההבדל חשוב גם לבני אדם: נמען שרואה "הסוד הזה כבר נצפה" יודע לספר לשולח, והשולח יכול לבדוק את יומן הביקורת ולהחליף את הסיסמה אם הצופה לא היה הנמען.

    סיומות קישור שקשה לנחש, ולמה זה לא מספיק

    סיומת הקישור היא מה שעומד בין האינטרנט הציבורי לבין טקסט מוצפן שעוד לא נקרא (המפתח נמצא בחלק שאחרי הסימן #, לא בשרת), ולכן היא חייבת לשאת אנטרופיה אמיתית. סיומת שנוצרת ממקור אקראי מאובטח קריפטוגרפית, למשל 22 תווים ב-base62, נושאת בערך 128 ביט. במצב כזה ניחוש שיטתי אינו מתקפה מציאותית בשום קצב בקשות ששרת היה עומד בו.

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

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

    אפליקציות צ'אט (Slack, Teams, iMessage) וסורקי אבטחה של אימייל מייצרים תצוגה מקדימה של URL: הם מורידים אותו בבקשת GET ברגע שההודעה נמסרת. אם נקודת הקצה של ההצגה מוחקת את הסוד ב-GET, הבוט של התצוגה המקדימה משמיד את הסוד לפני שאדם בכלל ראה אותו. זה הכשל הנפוץ ביותר בפועל בכלים פשטניים של מחיקה לאחר קריאה.

    ההגנות, כפי ש-LinkPilot מממש אותן:

    • הפרדה בין פעולת המטא-דאטה לפעולת ההצגה. בקשת GET לנקודת הקצה של ההצגה מחזירה רק את הסטטוס ואם נדרש ביטוי סיסמה. צפייה נרשמת רק בבקשת POST מפורשת, הפעולה שאדם מבצע כשהוא לוחץ על כפתור ההצגה.
    • שער ביטוי סיסמה (Pro). כשמוגדר ביטוי סיסמה, התוכן לא מוחזר לאף אחד, בוט או אדם, בלי ה-hash הנכון.
    • Cache-Control: no-store בכל תגובת הצגה, כך שגורמי ביניים אף פעם לא שומרים את תוכן הסוד במטמון.

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

    מחזור החיים, מצב אחר מצב

    מצבמה גורם לומה המבקר רואהאפשר לשחזר את התוכן?
    פעיליצירהעמוד ההצגה (בקשה לביטוי סיסמה אם הוגדר)כן: הצגה אחת זמינה
    נמחקהגעה למגבלת הצפיות"כבר נצפה והושמד", HTTP 410לא: התוכן נדרס
    פג תוקףחלף מועד התפוגהעמוד סטטוס של פג תוקףלא: נדרס בגישה הבאה
    בוטלהשולח מבטל את הקישור מלוח הבקרהמסרב להציג, HTTP 410לא: מיידי ולצמיתות

    כל מעבר בין מצבים נרשם ביומן הביקורת, תכונה של Pro ב-LinkPilot. סוגי האירועים הם created, viewed, failed_passphrase, burned, expired ו-revoked, כל אחד עם חותמת זמן וכתובת IP שעברה hash (SHA-256 עם salt שמתחלף מדי יום; כתובת ה-IP הגולמית לא נשמרת אף פעם). היומן הזה הוא מה שהופך את "נראה לי שזה הגיע" ל"נצפה ב-10:05 מהמדינה הצפויה, צפייה אחת, ואז נמחק".

    הרחבות נפוצות

    • שער ביטוי סיסמה: דרישה לביטוי סיסמה שנמסר בערוץ אחר לפני ההצגה, כך ש-URL שדלף לבדו אינו סוד שדלף. ב-LinkPilot זו תכונה של Pro ($29 לחודש), לצד תפוגה מותאמת אישית ויומן הביקורת; המחולל שעובד בלי חשבון לא יכול להגדיר ביטוי סיסמה. הגבילו את קצב הניסיונות: LinkPilot מאפשר 5 ניסיונות לכל IP לדקה לכל סוד, ורושם כל כישלון.
    • תפוגה לפי זמן: הסוד מת אחרי פרק זמן קבוע גם אם אף אחד לא קרא אותו. הטופס של LinkPilot למשתמשים מחוברים מציע 1 שעה, 24 שעות או 7 ימים בכל תוכנית, ותאריך ושעה מותאמים אישית בתוכנית Pro. המחולל שעובד בלי חשבון מגדיר 7 ימים כברירת מחדל. זה מכסה את המקרה של קישור שנשכח בתיבת הדואר, שמחיקה לאחר קריאה לא מכסה.
    • ביטול ידני: השולח יכול לבטל את הקישור לסוד שלא נקרא מלוח הבקרה ברגע שמשהו נראה לא בסדר. ביטוח זול למקרה של "שלחתי לאלכס הלא נכון".

    מקרי קצה שמבדילים מימושים טובים

    האדם הלא נכון פותח את הקישור ראשון

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

    בעיית צילום המסך

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

    טעינה מוקדמת וטעינה ספקולטיבית בדפדפן

    חלק מהדפדפנים והאפליקציות טוענים מראש URL שסביר שילחצו עליהם. כלי שבנוי נכון מוחק רק בבקשת POST מפורשת, ולכן טעינה מוקדמת מתנהגת בדיוק כמו בוט של תצוגה מקדימה: היא יכולה להוריד את מעטפת העמוד, אף פעם לא את התוכן.

    • תצטרכו את הערך שוב. אפשרות השחזור היא כל מטרת התכנון של מנהל סיסמאות, ובדיוק מה שקישור חד-פעמי נועד למנוע. השתמשו בפריט בכספת.
    • כמה נמענים צריכים את הסוד. קישור אחד, קריאה אחת. הפצה לכמה אנשים פירושה ליצור קישור לכל אדם (זה בסדר, ואפשר לעקוב אחריו ביומן), או להודות שאתם צריכים בקרת גישה משותפת.
    • מי שצורך את הסוד הוא מכונה. שירותים ו-CI צריכים לקרוא ממנהל סודות, לא ללחוץ על קישורים.

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

    מקורות

    כלים למאמר הזה

    שאלות נפוצות

    LinkPilot שולח קישור סודי שנמחק אחרי שפותחים אותו.

    מוצפן בדפדפן שלכם, מוצג פעם אחת ואז מושמד. ללא חשבון.

    יצירת קישור סודי

    לקריאה נוספת

    אבטחה

    קישורים עם תפוגה: הסוד מושמד, לא רק מפסיק להפנות

    קישור סודי עם תפוגה משמיד את הסוד במועד שנקבע; קישור מקוצר שמת רק מפסיק להפנות. איך בוחרים חלון זמן, ומה קורה במקרי הקצה.

    אבטחה

    הצפנה בצד הלקוח לשיתוף סודות: על מה היא מגינה ועל מה לא

    הצפנה בצד הלקוח מגינה על סוד שמור מפני המפעיל ומדליפת מסד נתונים, ותו לא: JavaScript זדוני, תוספים וקישור שדלף עוקפים אותה.

    אבטחה

    שיתוף חד-פעמי של פרטי התחברות בקליטת לקוחות

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