扩展程序有权访问浏览器中的特殊权限,因此对攻击者来说极具吸引力。如果某个扩展程序遭到入侵,该扩展程序的每位用户都会容易受到恶意和不必要的入侵。采用以下做法,确保扩展程序安全无虞并保护其用户。
保护开发者账号
扩展程序代码通过 Google 账号上传和更新。如果开发者的账号遭到入侵,攻击者可以直接向所有用户推送恶意代码。为了保护这些账号,请专门创建开发者账号,并启用双重身份验证,最好使用安全密钥。
有选择地保留群组
如果使用群组发布,请将群组限制为仅限可信开发者使用。请勿接受陌生人的会员申请。
绝不使用 HTTP
在请求或发送数据时,请避免使用 HTTP 连接。假设任何 HTTP 连接都会有窃听者或包含修改。应始终首选 HTTPS,因为它具有内置安全性,可规避大多数中间人攻击。
请求最低权限
Chrome 浏览器会限制扩展程序对清单中明确请求的权限的访问。扩展程序应仅注册其依赖的 API 和网站,从而尽可能减少其权限。应尽可能减少任意代码。
限制扩展程序的权限可限制潜在攻击者可利用的漏洞。
跨源 XMLHttpRequest
扩展程序只能使用 XMLHttpRequest 从自身和权限中指定的网域获取资源。
{
"name": "Very Secure Extension",
"version": "1.0",
"description": "Example of a Secure Extension",
"permissions": [
"/*",
"https://*.google.com/"
],
"manifest_version": 2
}
此扩展程序通过在权限中列出 "/*" 和 "https://*google.com/",请求访问 developer.chrome.com 和 Google 的子网域中的任何内容。即使扩展程序遭到入侵,它也只能与符合匹配模式的网站互动。攻击者将无法访问 "https://user_bank_info.com" 或与 "https://malicious_website.com" 互动。
限制清单字段
在清单中包含不必要的注册会产生漏洞,并使扩展程序更加显眼。将清单字段限制为扩展程序依赖的字段,并提供具体的字段注册。
可从外部连接
使用 externally_connectable 字段声明扩展程序将与哪些外部扩展程序和网页交换信息。限制扩展程序可以与哪些外部可信来源建立连接。
{
"name": "Super Safe Extension",
"externally_connectable": {
"ids": [
"iamafriendlyextensionhereisdatas"
],
"matches": [
"/*",
"https://*google.com/"
],
"accepts_tls_channel_id": false
},
...
}
可通过网络访问的资源
通过网络访问 web_accessible_resources 下的资源,会导致扩展程序可被网站和攻击者检测到。
{
...
"web_accessible_resources": [
"images/*.png",
"style/secure_extension.css",
"script/secure_extension.js"
],
...
}
可从 Web 访问的资源越多,潜在攻击者可利用的途径就越多。尽量减少这些文件的数量。
包含明确的内容安全政策
在清单中为扩展程序添加内容安全政策,以防止跨站脚本攻击。如果扩展程序仅加载来自自身的资源,请注册以下内容:
{
"name": "Very Secure Extension",
"version": "1.0",
"description": "Example of a Secure Extension",
"content_security_policy": "default-src 'self'"
"manifest_version": 2
}
如果扩展程序需要包含来自特定主机的脚本,则可以包含这些脚本:
{
"name": "Very Secure Extension",
"version": "1.0",
"description": "Example of a Secure Extension",
"content_security_policy": "default-src 'self' https://extension.resource.com"
"manifest_version": 2
}
避免使用可执行的 API
应使用更安全的替代方案来替换执行代码的 API。
document.write() 和 innerHTML
虽然使用 document.write() 和 innerHTML 动态创建 HTML 元素可能更简单,但这样一来,扩展程序以及扩展程序所依赖的网页就容易受到攻击者的攻击,他们可能会插入恶意脚本。请改为手动创建 DOM 节点,并使用 innerText 插入动态内容。
function constructDOM() {
let newTitle = document.createElement('h1');
newTitle.innerText = host;
document.appendChild(newTitle);
}
eval()
请尽可能避免使用 eval() 以防遭到攻击,因为 eval() 会执行传递给它的任何代码,而这些代码可能包含恶意内容。
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();
建议改用更安全、更快捷的方法,例如 JSON.parse()
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();
谨慎使用内容脚本
- 内容脚本是扩展程序中唯一能直接与网页互动的组成部分。 因此,恶意网页可能会操纵内容脚本所依赖的 DOM 部分,或利用令人意外的网络标准行为,例如命名项。
- 为了与网页的 DOM 进行交互,内容脚本需要在与网页相同的渲染器进程中执行。这使得内容脚本容易受到通过边信道攻击(例如 Spectre)泄露数据的攻击,并且如果恶意网页危害渲染器进程,内容脚本还容易被攻击者接管。
敏感工作应在专用进程中执行,例如扩展程序的后台脚本。避免意外向内容脚本公开扩展程序权限:
- 假设来自内容脚本的消息可能由攻击者精心设计(例如,验证并清理所有输入内容,并保护您的脚本免受跨站脚本攻击)。
- 假设发送到内容脚本的任何数据都可能会泄露到网页。请勿将敏感数据(例如扩展程序中的 Secret、来自其他 Web 源的数据、浏览记录)发送到内容脚本。
- 限制内容脚本可触发的特权操作的范围。不允许内容脚本触发对任意网址的请求或向扩展程序 API 传递任意实参(例如,不允许向
fetch或chrome.tabs.createAPI 传递任意网址)。
注册并清理输入
通过以下方式保护扩展程序免受恶意脚本的侵害:仅将监听器限制为扩展程序预期使用的监听器、验证传入数据的发送者,以及清理所有输入。
如果扩展程序需要接收来自外部网站或扩展程序的通信,则应仅注册 runtime.onRequestExternal。请务必验证发件人是否为可信来源。
// The ID of an external extension
const kFriendlyExtensionId = "iamafriendlyextensionhereisdatas";
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.id === kFriendlyExtensionId)
doSomething();
});
即使是来自扩展程序本身的通过 runtime.onMessage 事件发送的消息,也应仔细检查,以确保 MessageSender 不是来自遭入侵的内容脚本。
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.allowedAction)
console.log("This is an allowed action.");
});
通过清理用户输入和传入数据(即使是来自扩展程序本身和获批来源的数据),防止扩展程序执行攻击者的脚本。避免使用可执行 API。
function sanitizeInput(input) {
return input.replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
}