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

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

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

Mỗi tiện ích đang chạy tồn tại trong nguồn gốc bảo mật riêng. Không yêu cầu thêm đặc quyền, tiện ích này có thể sử dụng XMLHttpRequest để nhận tài nguyên trong quá trình cài đặt. Cho 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 một config_resources, tiện ích này có thể truy xuất nội dung 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 với chính nó, chẳng hạn như https://www.google.com, trình duyệt sẽ không cho phép việc này trừ phi tiện ích đã yêu cầu quyền truy cập phù hợp trên nhiều nguồn gốc quyền truy cập.

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

Bằng cách thêm mẫu so khớp máy chủ lưu trữ hoặc máy chủ lưu trữ (hoặc cả hai) vào phần quyền của tệp kê khai, tiện ích này 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 trên nhiều nguồn gốc 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 đối sánh, như sau:

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

Mẫu khớp "https://*/" cho phép truy cập HTTPS vào tất cả các miền có thể truy cập. Lưu ý rằng ở đây, hãy khớp mẫu sẽ tương tự như mẫu so khớp tập lệnh nội dung, nhưng mọi thông tin đường dẫn tuân theo máy chủ lưu trữ 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à giao thức. Nếu một tiện ích muốn vừa bảo mật vừa truy cập HTTP không an toàn vào một máy chủ hoặc nhóm máy chủ nhất định, thì máy 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 lỗ hổng bảo mật tập lệnh trên nhiều trang web

Khi sử dụng các tài nguyên được truy xuất 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ình trạng viết 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ư bên dưới:

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

Giới hạn quyền truy cập của tập lệnh nội dung đối với các yêu cầu từ nhiều nguồn gốc

Khi thực hiện các yêu cầu từ nhiều nguồn thay cho một tập lệnh nội dung, hãy thận trọng để ngăn chặn trang web độc hại có thể tìm cách mạo danh một tập lệnh nội dung. Đặc biệt, không cho phép các tập lệnh nội dung để yêu cầu một URL tuỳ ý.

Hãy xem xét một ví dụ trong đó tiện ích thực hiện yêu cầu trên nhiều nguồn gốc để cho phép tập lệnh nội dung tìm hiểu giá của một mặt hàng. Một cách tiếp cận (không an toàn) là yêu cầu tập lệnh nội dung chỉ định tài nguyên chính xác mà trang nền 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()));

Ở cách tiếp cận 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. Trang web độc hại có thể giả mạo các thư như vậy và đánh lừa tiện ích mở rộng cấp quyền truy cập vào các tài nguyên trên nhiều nguồn gốc.

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. Dưới đây, chỉ itemId do tập lệnh nội dung cung cấp chứ không phải URL đầy đủ.

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 thay vì HTTP

Ngoài ra, hãy đặc biệt thận trọng với các tài nguyên được truy xuất qua HTTP. Nếu tiện ích của bạn được sử dụng trên mạng thù địch, 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 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 bạn sửa đổi Chính sách bảo mật nội dung mặc định cho ứng dụng hoặc tiện ích bằng cách thêm content_security_policy vào tệp kê khai của mình, bạn sẽ 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 phép. Mặc dù chính sách mặc định không hạn chế kết nối tới máy chủ, hãy cẩn thận khi thêm lệnh connect-src hoặc default-src một cách rõ ràng.