XMLHttpRequest trên nhiều nguồn gốc

Các trang web thông thường có thể sử dụng đối tượng XMLHttpRequest để gửi và nhận dữ liệu từ các máy chủ từ xa, nhưng bị giới hạn bởi chính sách cùng nguồn gốc. Tập lệnh nội dung khởi tạo các yêu cầu thay cho nguồn gốc trang web mà tập lệnh nội dung đã được chèn vào. Do đó, tập lệnh nội dung cũng phải tuân theo chính sách cùng nguồn gốc. (Các tập lệnh nội dung đã phải tuân theo CORB kể từ Chrome 73 và CORS kể từ Chrome 83.) Nguồn gốc của tiện ích không bị giới hạn nhiều như vậy – một tập lệnh thực thi trong trang nền hoặc thẻ nền trước của tiện ích có thể giao tiếp với các máy chủ từ xa bên ngoài nguồn gốc của tiện ích, miễn là tiện ích yêu cầu quyền truy cập từ nhiều nguồn.

Nguồn gốc của tiện ích

Mỗi tiện ích đang chạy đều nằm trong nguồn gốc bảo mật riêng. Không cần yêu cầu thêm đặc quyền, tiện ích có thể dùng XMLHttpRequest để lấy tài nguyên trong quá trình cài đặt. Ví dụ: nếu một tiện ích chứa tệp cấu hình JSON có tên là config.json, trong thư mục config_resources, thì tiện ích đó có thể truy xuất nội dung của tệp như sau:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = handleStateChange; // Implemented elsewhere.
xhr.open("GET", chrome.extension.getURL('/config_resources/config.json'), true);
xhr.send();

Nếu tiện ích cố gắng sử dụng một nguồn gốc bảo mật khác ngoài chính nó, chẳng hạn như https://www.google.com, trình duyệt sẽ không cho phép trừ phi tiện ích đã yêu cầu các quyền thích hợp trên nhiều nguồn gốc.

Yêu cầu cấp quyền trên nhiều nguồn gốc

Bằng cách thêm máy chủ hoặc mẫu khớp máy chủ (hoặc cả hai) vào mục permissions của tệp manifest, tiện ích có thể yêu cầu quyền truy cập vào các máy chủ từ xa bên ngoài nguồn gốc của tiện ích.

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

Các giá trị quyền truy cập từ nhiều nguồn có thể là tên máy chủ đủ điều kiện, chẳng hạn như:

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

Hoặc chúng có thể là các mẫu so khớp, chẳng hạn như:

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

Mẫu so khớp "https://*/" cho phép truy cập HTTPS vào tất cả các miền có thể truy cập. Xin lưu ý rằng ở đây, các mẫu khớp tương tự như các mẫu khớp của tập lệnh nội dung, nhưng mọi thông tin về đường dẫn sau máy chủ đều bị bỏ qua.

Ngoài ra, xin lưu ý rằng quyền truy cập được cấp theo cả máy chủ và lược đồ. Nếu muốn có cả quyền truy cập HTTP an toàn và không an toàn vào một máy chủ hoặc nhóm máy chủ nhất định, tiện ích phải khai báo riêng các quyền:

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

Lưu ý về bảo mật

Tránh các lỗ hổng về tập lệnh trên nhiều trang web

Khi sử dụng các tài nguyên được truy xuất thông qua XMLHttpRequest, trang nền của bạn phải cẩn thận để không trở thành nạn nhân của tập lệnh trên nhiều trang web. Cụ thể, hãy tránh sử dụng các API nguy hiểm như sau:

var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // WARNING! Might be evaluating an evil script!
    var resp = eval("(" + xhr.responseText + ")");
    ...
  }
}
xhr.send();
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // WARNING! Might be injecting a malicious script!
    document.getElementById("resp").innerHTML = xhr.responseText;
    ...
  }
}
xhr.send();

Thay vào đó, hãy ưu tiên các API an toàn hơn không chạy tập lệnh:

var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // JSON.parse does not evaluate the attacker's scripts.
    var resp = JSON.parse(xhr.responseText);
  }
}
xhr.send();
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // innerText does not let the attacker inject HTML elements.
    document.getElementById("resp").innerText = xhr.responseText;
  }
}
xhr.send();

Hạn chế quyền truy cập của tập lệnh nội dung vào các yêu cầu từ nguồn khác

Khi thực hiện các yêu cầu trên nhiều nguồn thay cho một tập lệnh nội dung, hãy cẩn thận chống lại các trang web độc hại có thể cố gắng mạo danh một tập lệnh nội dung. Cụ thể, không cho phép tập lệnh nội dung yêu cầu một URL bất kỳ.

Hãy xem xét ví dụ về một tiện ích thực hiện yêu cầu trên nhiều nguồn gốc để cho phép một tập lệnh nội dung khám phá giá của một mặt hàng. Một phương pháp (không bảo mật) là để tập lệnh nội dung chỉ định chính xác tài nguyên mà trang nền sẽ tìm nạp.

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()));

Trong phương pháp trên, tập lệnh nội dung có thể yêu cầu tiện ích tìm nạp bất kỳ URL nào mà tiện ích có quyền truy cập. Một trang web độc hại có thể giả mạo những thông báo như vậy và lừa tiện ích cấp quyền truy cập vào các tài nguyên trên nhiều nguồn.

Thay vào đó, hãy thiết kế các trình xử lý thông báo để giới hạn những tài nguyên có thể tìm nạp. Ở bên dưới, chỉ có itemId được cung cấp bởi tập lệnh nội dung chứ không phải toàn bộ URL.

chrome.runtime.onMessage.addListener(
    function(request, sender, sendResponse) {
      if (request.contentScriptQuery == 'queryPrice') {
        var 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 => ...);

Ưu tiên HTTPS hơn HTTP

Ngoài ra, hãy đặc biệt cẩn thận với các tài nguyên được truy xuất qua HTTP. Nếu tiện ích của bạn được dùng trên một mạng thù địch, thì kẻ tấn công mạng (còn gọi là "man-in-the-middle") có thể sửa đổi phản hồi và có khả năng tấn công tiện ích của bạn. Thay vào đó, hãy ưu tiên sử dụng HTTPS bất cứ khi nào có thể.

Điều chỉnh chính sách bảo mật nội dung

Nếu sửa đổi Chính sách bảo mật nội dung mặc định cho các ứng dụng hoặc tiện ích bằng cách thêm thuộc tính content_security_policy vào tệp kê khai, bạn cần đảm bảo rằng mọi máy chủ lưu trữ mà bạn muốn kết nối đều được cho phép. Mặc dù chính sách mặc định không hạn chế các kết nối đến máy chủ lưu trữ, nhưng hãy cẩn thận khi thêm rõ ràng chỉ thị connect-src hoặc default-src.