หน้าเว็บทั่วไปสามารถใช้ fetch()
หรือ XMLHttpRequest
API เพื่อส่งและรับข้อมูลจากเซิร์ฟเวอร์ระยะไกลได้ แต่จะถูกจํากัดโดยนโยบายต้นทางเดียวกัน สคริปต์เนื้อหาจะส่งคำขอในนามของต้นทางเว็บที่มีการแทรกสคริปต์เนื้อหา ดังนั้นสคริปต์เนื้อหาจึงอยู่ภายใต้นโยบายต้นทางเดียวกันด้วย ต้นทางของส่วนขยายยังมีข้อจำกัด สคริปต์ที่ทำงานใน 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 เบราว์เซอร์จะไม่อนุญาต เว้นแต่ว่าส่วนขยายจะขอสิทธิ์ข้ามแหล่งที่มาที่เหมาะสม
ขอสิทธิ์ข้ามแหล่งที่มา
หากต้องการขอสิทธิ์เข้าถึงเซิร์ฟเวอร์ระยะไกลนอกต้นทางของส่วนขยาย ให้เพิ่มโฮสต์ รูปแบบการจับคู่ หรือทั้ง 2 อย่างลงในส่วน 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()
สร้างขึ้นเพื่อ Service Worker โดยเฉพาะ และเป็นไปตามเทรนด์เว็บที่กว้างขึ้นซึ่งหันไปใช้งานแบบไม่พร้อมกัน ส่วนขยาย XMLHttpRequest()
API ได้รับการสนับสนุนในส่วนขยายที่อยู่นอกโปรแกรมทำงานของบริการ และการเรียกใช้ API นี้จะเรียกใช้เครื่องจัดการการดึงข้อมูลของโปรแกรมทำงานของส่วนขยาย งานใหม่ควรใช้ fetch()
ทุกครั้งที่เป็นไปได้
ข้อควรพิจารณาด้านความปลอดภัย
หลีกเลี่ยงช่องโหว่ของ Cross-site Scripting
เมื่อใช้ทรัพยากรที่ดึงข้อมูลผ่าน 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;
จำกัดการเข้าถึงสคริปต์เนื้อหาสำหรับคำขอข้ามแหล่งที่มา
เมื่อส่งคำขอข้ามต้นทางในนามของสคริปต์เนื้อหา โปรดป้องกันหน้าเว็บที่เป็นอันตรายที่อาจพยายามแอบอ้างเป็นสคริปต์เนื้อหา โดยเฉพาะอย่างยิ่ง ไม่อนุญาตให้สคริปต์เนื้อหาขอ 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
ลงในไฟล์ Manifest คุณจะต้องตรวจสอบว่าโฮสต์ที่คุณต้องการเชื่อมต่อได้รับอนุญาต แม้ว่านโยบายเริ่มต้นจะไม่ได้จำกัดการเชื่อมต่อกับโฮสต์ แต่โปรดระมัดระวังเมื่อเพิ่มคำสั่ง connect-src
หรือ default-src
อย่างชัดเจน