מדיניות אותו המקור (SOP) ושיתוף משאבים ממקורות שונים (CORS)

אבטחת מידע היא אחד הנושאים הפחות מובנים למפתחים כמוני, שאינם האקרים. רובנו יודעים שיש משהו שנקרא CORS ושזה עלול לעשות לנו בעיות כאשר אנחנו מנסים לגשת מהאתר שלנו לשרת בדומיין אחר, או להיפך, כאשר מישהו אחר מנסה לגשת לשרת שלנו. מי שעובד עם Express יודע גם שיש middleware שפותר את הבעיה הזאת בקלות. אבל אם כל כך קל להתגבר על הבעיה הזאת, מדוע היא קיימת מלכתחילה? ואיך כותרות (headers) שכל שרת, גם אם הוא מכיל תוכן זדוני, יכול להוסיף לתשובות שלו יכולות להגן על המשתמשים מפני התקפות? שאלות אלו הציקו לי עד שהחלטתי ללמוד קצת את הנושא.

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

מהי "מדיניות אותו המקור" ומהי בעצם הבעיה שהיא נועדה לפתור?

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

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

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

הפרצה הפוטנציאלית הזאת קיימת בדפדפנים מאז הופעתה של ג'אווהסקריפט ב-1995, והיא זאת שהובילה את מפתחי הדפדפנים ליישם את מדיניות אותו המקור בכל הדפדפנים שפותחו מאז. (אלא אם כן אתם משתמשים בדפדפן שהורדתם מאיזשהו אתר רוסי פיראטי, ואז all bets are off).

מדיניות אותו המקור (SOP–Same Origin Policy)

לפי מדיניות אותו המקור (SOP–Same Origin Policy), קוד ג'אווהסקריפט שרץ בדף מסויים לא יכול לגשת למשאבים שהגיעו ממקור אחר. מקור, לצורך העניין, הוא השילוב של הפרוטוקול (או הסְכֵמָה), המחשב המארח, והפורט של הדף. לדוגמה, סקריפט שרץ בדף https://example.com רשאי לשלוח בקשה ל-URL הזה:

אבל לא ל-URLs האלה:

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

הקלות במדיניות

"אבל רגע…" אתם אולי אומרים בשלב הזה, "למה אתה אומר שאי אפשר לשלוח בקשות AJAX למקור אחר? רק אתמול כתבתי אפליקציה בג'אווהסקריפט שניגשת ל-https://covid19api.com, קוראת משם נתונים ובונה מהם גרפים של מספר מקרי הקורונה בכל העולם!" התשובה היא שהשרת של covid19api.com, בניגוד לשרת של הבנק שלכם, משתמש בכותרות (headers) מיוחדות כדי לאותת לדפדפן שלכם שאין סכנה בכך שהסקריפט שלכם ישלח אליו בקשות AJAX. כותרות אלה הן חלק ממנגנון CORS שהומצא כדי לאפשר לדפדפנים לחרוג במקרים מסויימים ממדיניות SOP בלי לסכן את המשתמשים.

CORS הוא מנגנון חדש יחסית. הוא הופיע לראשונה ב-2009 באינטרנט אקספלורר 8 ובפיירפוקס 3.5. מה עשו מפתחי ווב לפני כן? אני לא בטוח, כי בשנים האלו לא עסקתי בווב. אני מנחש שהשתמשו בצד השרת כבמעין פרוקסי שהעביר את הבקשות מה-API שלו ל-API החיצוני. עד שמתישהו בתחילת העשור הקודם מישהו הגה רעיון גאוני שנקרא JSONP.

JSONP–JSON with Padding

(פרק זה דן בטכניקה שכבר אינה בשימוש נפוץ אבל היא מעניינת מבחינה טכנית והיסטורית.)

חשוב להבין שמדיניות אותו המקור חלה רק על קוד ג'אווהסקריפט ורק בזמן שהוא רץ. תגיות HTML שגורמות לדפדפן לבקש משאבים נוספים כמו <img>, <link> ואפילו <script> אינן כפופות ל-SOP. תגיות <script> מעניינות במיוחד בהקשר זה מכיוון שאפשר לנצל את הסקופ הגלובלי כדי לשתף מידע ופונקציות בין תגיות <script> נפרדות.

נניח שאני כותב API שמחזיר מידע בפורמט JSON, וחושף אותו ב-URL הבא:

https://example.com/api/v1/data

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

var scriptElement = document.createElement('script');
scriptElement.src = 'https://example.com/api/v1/data';
document.body.appendChild(scriptElement);

כלומר, אני יוצר אלמנט חדש מסוג script, נותן לו את ה-URL הדרוש בתור src ומוסיף אותו ל-DOM. ברגע שהאלמנט נוסף ל-DOM, הדפדפן שולח בקשת GET ל-https://example.com/api/v1/data, מקבל תשובה ומתייחס אליה בתור סקריפט, כלומר, מנסה להריץ את הקוד. כאן יש בעיה קטנה כי אמנם כל מחרוזת JSON היא ביטוי חוקי בג'אווהסקריפט, אבל אם הביטוי הזה מופיע לבדו בסקריפט אין שום דרך לגשת לערך שלו. לכן, במקום לשלוח את ה-JSON לבד, השרת "מרפד" אותו בקריאה לפונקציה, כך:

callback({...});

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

CORS–Cross-Origin Resource Sharing

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

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

כאמור, CORS מבוסס על כותרות (headers) שמאפשרות לדפדפן להבחין בין משאבים רגישים כמו API של בנק לבין משאבים לא מזיקים כמו ה-API של covid19api.com. במאמר הזה אני לא רוצה לפרט את כל הכותרות ואת משמעותן, אלא רק להזכיר את שתי הכותרות החשובות ביותר שהן כותרת הבקשה (request header) Origin שמכילה את המקור (פרוטוקול + דומיין + פורט) ממנו נשלחה הבקשה וכותרת התגובה (response header) Access-Control-Allow-Origin, שאם הגישה בטוחה מכילה או את המקור הספציפי שממנו הגיעה הבקשה (זה שצוין ב-Origin) או את הערך המיוחד * שמשמעותו "הגישה בטוחה לבקשות מכל מקור".

בכל פעם שהדפדפן שולח בקשת AJAX פשוטה (באמצעות XHR או fetch) למשאב חיצוני, כלומר כזה שלא מגיע מאותו המקור שממנו הגיע הדף שיזם את הבקשה, הוא מחפש בתשובה של השרת את הכותרת Access-Control-Allow-Origin ולפיה הוא קובע אם להעביר את התשובה בחזרה לסקריפט. אם הכותרת לא נמצאת בתשובה, או שהערך שלה לא מכיל את המקור שממנו נשלחה הבקשה, הקריאה ל-fetch תחזיר סטטוס 0; כלומר הסקריפט אפילו לא יוכל לדעת אם הקריאה הצליחה או לא.

כל הבקשות האחרות דורשות שליחה של בקשה מקדימה ("preflight") מסוג OPTIONS שנועדה לברר אם אפשר לשלוח את הבקשה העיקרית ללא סיכון. הדיאלוג המלא בין הלקוח לשרת נראה כך:

במקרה הזה נדרשת בקשת preflight מכיוון שהבקשה העיקרית עומדת לכלול תוכן מסוג text/xml וכן כותרת לא סטנדרטית, X-PINGOTHER. פרט ל-Origin, הדפדפן מציין (בכותרת Access-Control-Request-Method) מה סוג הבקשה שהוא מתכוון לשלוח בהמשך ואילו כותרות (Access-Control-Request-Headers) הוא מתכוון לצרף לבקשה. השרת עונה עם סטטוס 204 No Content ומצרף את כל כותרות Access-Control-Allow-* הרלוונטיות. לאחר מכן השרת מאפשר ללקוח לשלוח בקשות העומדות בתנאים הדרושים במשך פרק זמן מסוים (Access-Control-Max-Age) ללא צורך בשליחה נוספת של בקשת OPTIONS.

האתר Will it CORS מסביר את התהליך באופן אינטראקטיבי.

שאלות ותשובות

  • שאלה: איך מדיניות אותו המקור, שאכיפתה תלויה אך ורק ברצונם הטוב של מפתחי הדפדפנים, יכולה למנוע מהאקרים לפרוץ לאתרי אינטרנט? הרי אין שום בעיה למצוא (או אפילו לפתח לבד) דפדפן שלא מיישם אותה?
  • תשובה: SOP לא נועדה למנוע מהאקרים לגשת בעצמם למשאבים רגישים. היא נועדה למנוע מהם לנצל את הדפדפנים של אנשים אחרים כדי לגשת בשמם לאותם משאבים. כל הדפדפנים הנפוצים מיישמים אותה.
  • שאלה: איך אפשר, בעזרת CORS, להבדיל בין אתרים תמימים לאתרים זדוניים?
  • תשובה: אי אפשר, אבל זה לא מה ש-CORS נועד לעשות. CORS נועד להבדיל בין URLs לגיטימיים, כמו למשל ה-API של הבנק שלכם, שהאקרים עלולים לנצל כדי לגרום נזק, לבין URLs לגיטימיים אחרים שלא יכולים לגרום שום נזק.
  • שאלה: מדוע מדיניות אותו המקור לא חלה, מכל הדברים, דווקא על תגיות <script>?
  • תשובה: כאמור, מטרתה של מדיניות SOP אינה לחסום סקריפטים זדוניים (כי אין שום דרך מעשית לזהות סקריפטים כאלה) אלא למנוע מהסקריפטים שכבר רצים בדפדפן לגשת למשאבים רגישים כמו ה-API של הבנק שלכם. SOP מגבילה רק גישה באמצעות קוד JS ולא באמצעות תגיות HTML. יחד עם זאת, ניתן לחסום סקריפטים שלא כוללים את הכותרת Access-Control-Allow-Origin באמצעות התכונה (attribute) crossorigin של תגית ה-<script>.
  • שאלה: איך אפשר למנוע מהדפדפן לגשת ל-URLs מסוכנים לפני שיודעים שהם מסוכנים? הרי CORS מבדיל בין URL מסוכן ל-URL בטוח רק על פי הכותרות שחוזרות מהשרת, ואז כבר מאוחר מדי, לא?
  • תשובה: מנגנון CORS מבדיל בין בקשות "פשוטות" לבקשות "מסובכות". בקשות פשוטות נשלחות ללא הכנה מוקדמת, ואם התשובה לא מכילה את הכותרות הדרושות הדפדפן פשוט לא מעביר אותה בחזרה לסקריפט. בקשות "מסובכות" דורשות מהדפדפן לשלוח בקשה מקדימה מסוג OPTIONS כדי לדעת אם CORS מאפשר לו בכלל לשלוח את הבקשה העיקרית.

מקורות

התחביר המוזר של טיפוסי נתונים ב-C

ב-C יש עשרה טיפוסי נתונים בסיסיים:

  • signed/unsigned char
  • signed/unsigned short
  • signed/unsigned int
  • signed/unsigned long
  • float
  • double

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

 

#include <stdio.h>
#include <stdlib.h>

int main() {
    int (*a)[12] = malloc(120 * sizeof(int));
    for (int i = 0; i < 10; i++) {
        for (int j = 0; j < 12; j++) {
            a[i][j] = i * j;
        }
    }
    printf("5 * 6 == %d", a[5][6]); // Prints: 5 * 6 == 30
    free(a);
    return 0;
}

מצביע לפונקציה

הנה עוד דוגמאות:

אם a הוא… …אז כדי לקבל int, נשתמש בביטוי… ולכן, כדי להצהיר על a, צריך לכתוב… הערות
מערך של 8 מצביעים לפונקציות שמחזירות int (*a[0])() int (*a[8])(); יש צורך בסוגריים כי בלעדיהם האופרטור () (קריאה לפונקציה) קודם לאופרטור * (dereference)

יש ב-C שתי דרכים להצהיר על פונקציה:

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

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

#include <stdio.h>
#include <stdlib.h>

/* K&R (old) syntax */
int foo() {
return 0;
}

/* ANSI (new) syntax */
int bar(void) {
return 0;
}

int frobozz(int a, int *b) {
return a * *b;
}

int main(void) {
int (*a)() = foo, (*b)() = bar, (*c)() = frobozz; // OK
int (*d)(void) = foo; // warning C4113: 'int (__cdecl *)()' differs in parameter lists from 'int (__cdecl *)(void)'
int (*e)(int, int *) = frobozz; // OK
int (*f)(void) = frobozz; // warning C4113: 'int (__cdecl *)(int,int *)' differs in parameter lists from 'int (__cdecl *)(void)'
int (*g)(int, int *) = foo; // warning C4113: 'int (__cdecl *)()' differs in parameter lists from 'int (__cdecl *)(int,int *)'
int (*h)(int, int *) = bar; // warning C4113: 'int (__cdecl *)(void)' differs in parameter lists from 'int (__cdecl *)(int,int *)'
return 0;
}