跨源网络请求

常规网页可以使用 fetch()XMLHttpRequest API 从远程服务器发送和接收数据,但受到同源政策的限制。内容脚本会代表已注入内容脚本的网页源发起请求,因此内容脚本也受同源政策的约束。扩展程序的来源不受限制。在扩展程序 Service Worker 或前台标签页中执行的脚本可以与其源之外的远程服务器通信,前提是该扩展程序请求跨源权限。

扩展程序来源

每个正在运行的扩展程序都存在于各自独立的安全源中。该扩展程序无需请求额外的权限,即可调用 fetch() 来获取安装范围内的资源。例如,如果某个扩展程序在 config_resources/ 文件夹中包含一个名为 config.json 的 JSON 配置文件,则该扩展程序可以检索该文件的内容,如下所示:

const response = await fetch('/config_resources/config.json');
const jsonData = await response.json();

如果此扩展程序尝试使用其自身以外的安全来源(例如 https://www.google.com),除非此扩展程序已请求适当的跨源权限,否则浏览器将禁止该来源。

请求跨源权限

如需请求访问扩展程序源以外的远程服务器,请将主机和/或匹配格式添加到清单文件的 host_permissions 部分。

{
  "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 创建的,遵循远离同步操作的更广泛的网络趋势。Service Worker 之外的扩展支持 XMLHttpRequest() API,调用它会触发扩展 Service Worker 的提取处理程序。新工作应尽可能优先考虑 fetch()

安全注意事项

避免出现跨站脚本攻击漏洞

使用通过 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;

仅允许跨源请求访问内容脚本

代表内容脚本执行跨源请求时,请务必防范可能试图冒充内容脚本的恶意网页。特别是,不允许内容脚本请求任意网址。

举例而言,扩展程序会执行跨源请求,让内容脚本发现商品的价格。一种不太安全的方法是让内容脚本指定后台网页要提取的确切资源。

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

在上述方法中,内容脚本可以要求扩展程序提取扩展程序有权访问的任何网址。恶意网页可能会伪造此类消息并诱骗扩展程序授予对跨源资源的访问权限。

请改为设计消息处理程序,以限制可提取的资源。如下所示,内容脚本仅提供 itemId,而非完整网址。

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 属性来修改扩展程序的默认内容安全政策,则需要确保允许您想要连接的所有主机。虽然默认政策不限制与主机的连接,但在明确添加 connect-srcdefault-src 指令时请务必小心。