คำขอเครือข่ายแบบข้ามต้นทาง

หน้าเว็บปกติสามารถใช้ API fetch() หรือ XMLHttpRequest เพื่อส่งและรับข้อมูลจากเซิร์ฟเวอร์ระยะไกลได้ แต่จะถูกจำกัดโดยนโยบายต้นทางเดียวกัน Content Script จะเริ่มคำขอ ในนามของต้นทางเว็บที่ Content Script ถูกแทรกเข้าไป ดังนั้น Content Script จึงอยู่ภายใต้นโยบายต้นทางเดียวกันด้วย แต่ต้นทางของส่วนขยายไม่ได้จำกัดอยู่เพียงเท่านั้น สคริปต์ ที่เรียกใช้ใน 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 ระบบจะถือว่าเป็นการขอคำขอข้ามต้นทาง เว้นแต่ส่วนขยายจะมีสิทธิ์เข้าถึงโฮสต์ ระบบจะถือว่าคำขอข้ามต้นทางเป็นคำขอข้ามต้นทางเสมอใน Content Script แม้ว่าส่วนขยายจะมีสิทธิ์เข้าถึงโฮสต์ก็ตาม

ขอสิทธิ์ข้ามต้นทาง

หากต้องการขอสิทธิ์เข้าถึงเซิร์ฟเวอร์ระยะไกลนอกต้นทางของส่วนขยาย ให้เพิ่มโฮสต์ รูปแบบการจับคู่ หรือทั้ง 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 สำหรับโดเมนที่เข้าถึงได้ทั้งหมด โปรดทราบว่าในที่นี้ รูปแบบการจับคู่ จะคล้ายกับรูปแบบการจับคู่ Content Script แต่ระบบจะไม่สนใจข้อมูลเส้นทางใดๆ ที่อยู่หลังโฮสต์

นอกจากนี้ โปรดทราบว่าสิทธิ์เข้าถึงจะได้รับทั้งจากโฮสต์และจากรูปแบบ หากส่วนขยายต้องการเข้าถึง HTTP ทั้งแบบปลอดภัยและไม่ปลอดภัยในโฮสต์หรือชุดโฮสต์ที่กำหนด ส่วนขยายจะต้องประกาศสิทธิ์แยกกัน ดังนี้

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

Fetch() เทียบกับ XMLHttpRequest()

fetch() สร้างขึ้นสำหรับ Service Worker โดยเฉพาะ และเป็นไปตามแนวโน้มที่กว้างขึ้นของเว็บที่หลีกเลี่ยงการดำเนินการแบบซิงโครนัส XMLHttpRequest() API รองรับในส่วนขยายที่อยู่นอก Service Worker และการเรียกใช้จะทริกเกอร์ตัวแฮนเดิลการดึงข้อมูลของ Service Worker ของส่วนขยาย งานใหม่ควรใช้ fetch() ทุกครั้งที่เป็นไปได้

ข้อควรพิจารณาด้านความปลอดภัย

หลีกเลี่ยงช่องโหว่ของ Cross-site Scripting

เมื่อใช้ทรัพยากรที่ดึงข้อมูลผ่าน fetch() เอกสารนอกหน้าจอ แผงด้านข้าง หรือป๊อปอัปควรระมัดระวังไม่ให้ตกเป็นเหยื่อของ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;

จำกัดการเข้าถึง Content Script สำหรับคำขอแบบข้ามต้นทาง

เมื่อส่งคำขอข้ามต้นทางในนามของ Content Script ให้ระมัดระวังป้องกัน หน้าเว็บที่เป็นอันตรายซึ่งอาจพยายามแอบอ้างเป็น Content Script โดยเฉพาะอย่างยิ่ง อย่าอนุญาตให้ สคริปต์เนื้อหาขอ URL ที่กำหนดเอง

ลองพิจารณาตัวอย่างที่ส่วนขยายส่งคำขอข้ามต้นทางเพื่อให้ Content Script ค้นหาราคาของสินค้า วิธีที่ไม่ค่อยปลอดภัยวิธีหนึ่งคือการให้ Content Script ระบุ ทรัพยากรที่แน่นอนซึ่งหน้าพื้นหลังจะดึงข้อมูล

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 อย่างชัดเจน