XMLHttpRequest ממקורות שונים

בדפי אינטרנט רגילים אפשר להשתמש באובייקט XMLHttpRequest כדי לשלוח ולקבל נתונים משרתים מרוחקים, אבל הם מוגבלים על ידי אותה מדיניות מקור. סקריפטים של תוכן יוזמים בקשות מטעם מקור האינטרנט שאליו הוחדר סקריפט התוכן, ולכן גם סקריפטים של תוכן כפופים לאותה מדיניות מקור. (סקריפטים של תוכן כפופים ל-CORB מאז Chrome 73 ו-CORS החל מ-Chrome 83). המקורות של התוסף לא כל כך מוגבלים – סקריפט שפועל בדף הרקע של התוסף או בכרטיסייה בחזית של התוסף יכול לתקשר עם שרתים מרוחקים מחוץ למקור שלו, כל עוד התוסף מבקש הרשאות בין מקורות.

מקור התוסף

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

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = handleStateChange; // Implemented elsewhere.
xhr.open("GET", chrome.extension.getURL('/config_resources/config.json'), true);
xhr.send();

אם התוסף מנסה להשתמש במקור אבטחה שאינו עצמו, למשל https://www.google.com, הדפדפן חוסם את האפשרות הזו אלא אם התוסף ביקש את ההרשאות המתאימות ממקורות שונים.

בקשה להרשאות ממקורות שונים

כשמוסיפים דפוסי התאמה למארחים או למארחים (או שניהם) בקטע ההרשאות של קובץ המניפסט, התוסף יכול לבקש גישה לשרתים מרוחקים מחוץ למקור.

{
  "name": "My extension",
  ...
  "permissions": [
    "https://www.google.com/"
  ],
  ...
}

ערכי הרשאות בין מקורות יכולים להיות שמות מארח מלאים, כמו אלה:

  • "https://www.google.com/"
  • "https://www.gmail.com/"

לחלופין, הם יכולים להיות תואמים לדפוסים, כגון:

  • "https://*.google.com/"
  • "https://*/"

דפוס התאמה של 'https://*/' מאפשר גישת HTTPS לכל הדומיינים שניתן להגיע אליהם. שימו לב שכאן, דפוסי ההתאמה דומים לתבניות ההתאמה של סקריפטים של תוכן, אבל המערכת מתעלמת מפרטי הנתיב שמופיע אחרי המארח.

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

"permissions": [
  "http://www.google.com/",
  "https://www.google.com/"
]

שיקולי אבטחה

הימנעות מנקודות חולשה של סקריפטים חוצי-אתרים

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

var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // WARNING! Might be evaluating an evil script!
    var resp = eval("(" + xhr.responseText + ")");
    ...
  }
}
xhr.send();
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // WARNING! Might be injecting a malicious script!
    document.getElementById("resp").innerHTML = xhr.responseText;
    ...
  }
}
xhr.send();

במקום זאת, עדיף להשתמש בממשקי API בטוחים יותר שלא מריצים סקריפטים:

var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // JSON.parse does not evaluate the attacker's scripts.
    var resp = JSON.parse(xhr.responseText);
  }
}
xhr.send();
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // innerText does not let the attacker inject HTML elements.
    document.getElementById("resp").innerText = xhr.responseText;
  }
}
xhr.send();

הגבלת הגישה של סקריפט התוכן לבקשות ממקורות שונים

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

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

chrome.runtime.onMessage.addListener(
    function(request, sender, sendResponse) {
      if (request.contentScriptQuery == 'fetchUrl') {
        // WARNING: SECURITY PROBLEM - a malicious web page may abuse
        // the message handler to get access to arbitrary cross-origin
        // resources.
        fetch(request.url)
            .then(response => response.text())
            .then(text => sendResponse(text))
            .catch(error => ...)
        return true;  // Will respond asynchronously.
      }
    });
chrome.runtime.sendMessage(
    {contentScriptQuery: 'fetchUrl',
     url: 'https://another-site.com/price-query?itemId=' +
              encodeURIComponent(request.itemId)},
    response => parsePrice(response.text()));

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

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

chrome.runtime.onMessage.addListener(
    function(request, sender, sendResponse) {
      if (request.contentScriptQuery == 'queryPrice') {
        var url = 'https://another-site.com/price-query?itemId=' +
            encodeURIComponent(request.itemId);
        fetch(url)
            .then(response => response.text())
            .then(text => parsePrice(text))
            .then(price => sendResponse(price))
            .catch(error => ...)
        return true;  // Will respond asynchronously.
      }
    });
chrome.runtime.sendMessage(
    {contentScriptQuery: 'queryPrice', itemId: 12345},
    price => ...);

העדפת HTTPS על פני HTTP

בנוסף, היזהר במיוחד ממשאבים שאוחזרו באמצעות HTTP. אם התוסף שלכם משמש ברשת עוינת, תוקף ברשת (שנקרא גם "man-in-the-middle") עלול לשנות את התגובה, ואולי לתקוף את התוסף שלכם. במקום זאת, מומלץ להשתמש ב-HTTPS כשהדבר אפשרי.

התאמה של המדיניות בנושא אבטחת תוכן

אם משנים את מדיניות אבטחת התוכן שמוגדרת כברירת מחדל עבור אפליקציות או תוספים על ידי הוספת המאפיין content_security_policy למניפסט, חשוב לוודא שכל המארחים שרוצים לקשר אליהם מורשים. מדיניות ברירת המחדל לא מגבילה את החיבורים למארחים, אבל כדאי להיזהר כשמוסיפים באופן מפורש את ההוראות connect-src או default-src.