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

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

מקור התוסף

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

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

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

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

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

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

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

Fetch() לעומת XMLHttpRequest()

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

שיקולי אבטחה

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

כשמשתמשים במשאבים שאוחזרו באמצעות fetch(), צריך להיזהר במסמך מחוץ למסך, בחלונית הצדדית או בחלון הקופץ, כדי שלא יהיו חשופים לסקריפטים חוצי-אתרים. באופן ספציפי, מומלץ להימנע משימוש בממשקי 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;

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

כשמבצעים בקשות ממקורות שונים מטעם סקריפט תוכן, חשוב להגן מפני דפי אינטרנט זדוניים שעשויים לנסות להתחזות לסקריפט תוכן. באופן ספציפי, אל תאפשרו לסקריפטים של תוכן לבקש כתובת 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 למניפסט, צריך לוודא שכל המארחים שאתם רוצים להתחבר אליהם מורשים. מדיניות ברירת המחדל לא מגבילה חיבורים למארחים, אבל כדאי להיזהר כשמוסיפים במפורש את ההוראות connect-src או default-src.