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

דפי אינטרנט רגילים יכולים להשתמש בממשקי ה-API‏ fetch() או XMLHttpRequest כדי לשלוח ולקבל נתונים משרתים מרוחקים, אבל הם מוגבלים על ידי מדיניות אותו המקור. סקריפטים של תוכן יוזמים בקשות בשם מקור האינטרנט שאליו הוחדר הסקריפט של התוכן, ולכן סקריפטים של תוכן כפופים גם למדיניות של אותו מקור. המקורות של התוספים לא כל כך מוגבלים. סקריפט שמופעל בקובץ שירות (service worker) של תוסף או בכרטיסייה בחזית יכול לתקשר עם שרתים מרוחקים מחוץ למקור שלו, בתנאי שהתוסף מבקש הרשאות מארח.

מקור התוסף

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

const response = await fetch('/config_resources/config.json');
const jsonData = await response.json();

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

בקשת הרשאות ל-CORS

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

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

ערכי הרשאות חוצי-מקור יכולים להיות שמות מארחים מוגדרים במלואם, כמו אלה:

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

או שהם יכולים להיות דפוסי התאמה, כמו אלה:

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

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

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

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

‫Fetch() לעומת XMLHttpRequest()

fetch() נוצר במיוחד בשביל Service Workers, והוא חלק ממגמה רחבה יותר באינטרנט של מעבר מפעולות סינכרוניות. ה-API‏ XMLHttpRequest() נתמך בתוספים מחוץ לקובץ השירות (service worker), והפעלתו מפעילה את handler האחזור של קובץ השירות של התוסף. בכל מקום שאפשר, עדיף להשתמש ב-fetch() בעבודה חדשה.

שיקולי אבטחה

הימנעות מפרצות אבטחה מסוג XSS‏ (cross-site scripting)

כשמשתמשים במשאבים שאוחזרו באמצעות fetch(), צריך לוודא שהמסמך, החלונית הצדדית או החלון הקופץ מחוץ למסך לא יהיו חשופים לפרצת אבטחה XSS‏ (cross-site scripting). בפרט, מומלץ להימנע משימוש בממשקי API מסוכנים כמו innerHTML. לדוגמה:

const response = await fetch("https://api.example.com/data.json");
const jsonData = await response.json();
// WARNING! Might be injecting a malicious script!
document.getElementById("resp").innerHTML = jsonData;
    ...

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

const response = await fetch("https://api.example.com/data.json");
const jsonData = await response.json();
// JSON.parse does not evaluate the attacker's scripts.
let resp = JSON.parse(jsonData);

const response = await fetch("https://api.example.com/data.json");
const jsonData = response.json();
// textContent does not let the attacker inject HTML elements.
document.getElementById("resp").textContent = jsonData;

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

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

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

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 שהתוסף יכול לגשת אליה. דף אינטרנט זדוני עשוי לזייף הודעות כאלה ולגרום לתוסף לתת גישה למשאבים ממקורות שונים.

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

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    if (request.contentScriptQuery == 'queryPrice') {
      const 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 שמוגדרת כברירת מחדל לתוסף על ידי הוספת מאפיין content_security_policy למניפסט, צריך לוודא שכל המארחים שאליהם רוצים להתחבר מורשים. מדיניות ברירת המחדל לא מגבילה את החיבורים למארחים, אבל צריך להיזהר כשמוסיפים באופן מפורש את ההנחיות connect-src או default-src.