常规网页可以使用 fetch() 或 XMLHttpRequest API 从远程
服务器发送和接收数据,但会受到 同源政策 的限制。内容脚本会代表内容脚本注入到的网站源发起请求,因此内容脚本也受同源政策的约束。扩展程序源站的限制较少。在扩展程序 Service Worker 或前台标签页中执行的脚本可以与源之外的远程服务器通信,前提是扩展程序请求了主机权限。
扩展程序源站
每个正在运行的扩展程序都存在于其自己的独立安全源站中。在不请求额外权限的情况下,扩展程序可以调用 fetch() 来获取其安装中的资源。例如,如果扩展程序包含一个名为 config.json 的 JSON 配置文件,该文件位于 config_resources/ 文件夹中,则扩展程序可以按如下方式检索该文件的内容:
const response = await fetch('/config_resources/config.json');
const jsonData = await response.json();
如果扩展程序尝试从其自身以外的安全源站(例如 https://www.google.com)请求内容,除非扩展程序具有主机权限,否则此请求将被视为非同源请求请求。在内容脚本中,跨源站请求始终被视为跨源站请求,即使扩展程序具有主机权限也是如此。
请求跨源站权限
如需请求访问扩展程序源站之外的远程服务器,请将主机、匹配模式、 或两者都添加到host_permissions文件的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 而创建,并遵循更广泛的 Web 趋势,即避免同步操作。Service Worker 之外的扩展程序支持 XMLHttpRequest() API,调用该 API 会触发扩展程序 Service Worker 的 fetch 处理程序。新工作应尽可能使用 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-src 或 default-src 指令时,请务必谨慎。