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ừ các máy chủ từ xa, nhưng bị hạn chế bởi cùng một chính sách về nguồn gốc. Tập lệnh nội dung khởi tạo 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, do đó tập lệnh nội dung cũng phải tuân theo cùng một chính sách nguồn gố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 như vậy – một tập lệnh thực thi trên trang nền của tiện ích hoặc thẻ trên nền trước 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 trên nhiều nguồn gốc.

Nguồn gốc của phần mở rộng

Mỗi tiện ích đang chạy đều tồn tại trong một nguồn gốc bảo mật riêng. Nếu không yêu cầu đặc quyền bổ sung, tiện ích có thể sử dụng XMLHttpRequest để nhận 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, thì trong thư mục config_resources, 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, thì trình duyệt sẽ không cho phép tiện ích đó trừ phi tiện ích yêu cầu cấp quyền thích hợp trên nhiều nguồn gốc.

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

Bằng cách thêm máy chủ hoặc mẫu so khớp máy chủ lưu trữ (hoặc cả hai) vào phần quyền của tệp tệp kê khai, 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 mình.

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

Giá trị của 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 trùng khớp, chẳng hạn như:

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

Mẫu khớp "https://*/" cho phép HTTPS truy cập vào tất cả các miền có thể truy cập. Lưu ý rằng ở đây, mẫu so khớp 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 đi sau máy chủ sẽ bị bỏ qua.

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

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

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

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

Khi sử dụng 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ập lệnh chéo trang web. Cụ thể, hãy tránh sử dụng các API nguy hiểm, chẳng hạn như:

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 mà 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 tập lệnh nội dung vào các yêu cầu nhiều nguồn gốc

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

Hãy xem xét ví dụ trong đó một tiện ích thực hiện yêu cầu nhiều nguồn gốc để cho phép 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 an toàn) là yêu cầu tập lệnh nội dung chỉ định đúng 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 các 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 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 các tài nguyên có thể tìm nạp. Bên dưới, chỉ có itemId là 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 hơn HTTP

Ngoài ra, hãy đặc biệt chú ý đến những 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ột mạng máy 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 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 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ủ 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ế việc kết nối với các máy chủ, nhưng hãy cẩn thận khi thêm lệnh connect-src hoặc default-src một cách rõ ràng.