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