基本信息
浏览器扩展是用 JavaScript 编写的,并在后台由浏览器加载。它有自己的 DOM ,但可以与其他网站的 DOM 交互。这意味着它可能会危害其他网站的机密性、完整性和可用性 (CIA)。
主要组件
扩展布局在可视化时效果最佳,包含三个组件。让我们深入了解每个组件。
内容脚本
每个内容脚本可以直接访问 单个网页 的 DOM,因此暴露于 潜在恶意输入 。然而,内容脚本除了能够向扩展核心发送消息外,没有其他权限。
扩展核心
扩展核心包含大部分扩展权限/访问,但扩展核心只能通过 XMLHttpRequest 和内容脚本与网页内容交互。此外,扩展核心无法直接访问主机机器。
本地二进制
扩展允许一个本地二进制文件,可以 以用户的完全权限访问主机机器。 本地二进制通过 Flash 和其他浏览器插件使用的标准 Netscape 插件应用程序编程接口 (NPAPI ) 与扩展核心交互。
边界
为了获得用户的完全权限,攻击者必须说服扩展将恶意输入从内容脚本传递到扩展核心,并从扩展核心传递到本地二进制。
扩展的每个组件之间由 强保护边界 隔离。每个组件在 单独的操作系统进程 中运行。内容脚本和扩展核心在 沙箱进程 中运行,这些进程对大多数操作系统服务不可用。
此外,内容脚本通过 在单独的 JavaScript 堆中运行 与其关联的网页分离。内容脚本和网页可以 访问相同的底层 DOM ,但两者 从不交换 JavaScript 指针 ,防止 JavaScript 功能的泄露。
manifest.json
Chrome 扩展只是一个带有 .crx 文件扩展名 的 ZIP 文件夹。扩展的核心是位于文件夹根目录的 manifest.json
文件,该文件指定布局、权限和其他配置选项。
示例:
Copy {
"manifest_version" : 2 ,
"name" : "My extension" ,
"version" : "1.0" ,
"permissions" : [
"storage"
] ,
"content_scripts" : [
{
"js" : [
"script.js"
] ,
"matches" : [
"https://example.com/*" ,
"https://www.example.com/*"
] ,
"exclude_matches" : [ "*://*/*business*" ] ,
}
] ,
"background" : {
"scripts" : [
"background.js"
]
} ,
"options_ui" : {
"page" : "options.html"
}
}
content_scripts
内容脚本在用户导航到匹配页面 时加载 ,在我们的例子中,任何匹配**https://example.com/*
表达式且不匹配 *://*/*/business*
正则表达式的页面。它们 像页面自己的脚本**一样执行,并且可以任意访问页面的文档对象模型 (DOM) 。
Copy "content_scripts" : [
{
"js" : [
"script.js"
] ,
"matches" : [
"https://example.com/*" ,
"https://www.example.com/*"
] ,
"exclude_matches" : [ "*://*/*business*" ] ,
}
],
为了包含或排除更多的 URL,您还可以使用 include_globs
和 exclude_globs
。
这是一个示例内容脚本,当使用 存储 API 从扩展的存储中检索 message
值时,将向页面添加一个解释按钮。
Copy chrome . storage . local .get ( "message" , result =>
{
let div = document .createElement ( "div" );
div .innerHTML = result .message + " <button>Explain</button>" ;
div .querySelector ( "button" ) .addEventListener ( "click" , () =>
{
chrome . runtime .sendMessage ( "explain" );
});
document . body .appendChild (div);
});
当点击此按钮时,通过利用runtime.sendMessage() API ,内容脚本向扩展页面发送消息。这是由于内容脚本在直接访问API方面的限制,storage
是少数例外之一。对于超出这些例外的功能,消息被发送到扩展页面,内容脚本可以与之通信。
要在Chrome中查看和调试内容脚本,可以通过选项 > 更多工具 > 开发者工具访问Chrome开发者工具菜单,或按Ctrl + Shift + I。
当开发者工具显示后,点击源 选项卡,然后点击内容脚本 选项卡。这允许观察来自各种扩展的运行内容脚本,并设置断点以跟踪执行流程。
注入的内容脚本
请注意,内容脚本不是强制性的 ,也可以通过**tabs.executeScript
** 动态 注入 脚本并在网页中以编程方式注入 。这实际上提供了更细粒度的控制 。
对于内容脚本的程序化注入,扩展需要对要注入脚本的页面具有主机权限 。这些权限可以通过在扩展的清单中请求它们 或通过activeTab 临时获得。
示例基于activeTab的扩展
Copy {
"name" : "My extension" ,
...
"permissions" : [
"activeTab" ,
"scripting"
] ,
"background" : {
"service_worker" : "background.js"
} ,
"action" : {
"default_title" : "Action Button"
}
}
Copy // content-script.js
document . body . style .backgroundColor = "orange" ;
//service-worker.js - Inject the JS file
chrome . action . onClicked .addListener ((tab) => {
chrome . scripting .executeScript ({
target : { tabId : tab .id } ,
files : [ "content-script.js" ]
});
});
Copy //service-worker.js - Inject a function
function injectedFunction () {
document . body . style .backgroundColor = "orange" ;
}
chrome . action . onClicked .addListener ((tab) => {
chrome . scripting .executeScript ({
target : {tabId : tab .id} ,
func : injectedFunction ,
});
});
示例:带脚本权限
Copy // service-workser.js
chrome . scripting .registerContentScripts ([{
id : "test" ,
matches : [ "https://*.example.com/*" ] ,
excludeMatches : [ "*://*/*business*" ] ,
js : [ "contentScript.js" ] ,
}]);
// Another example
chrome . tabs .executeScript (tabId , { file : "content_script.js" });
为了包含或排除更多的 URL,您还可以使用 include_globs
和 exclude_globs
。
内容脚本 run_at
run_at
字段控制 JavaScript 文件何时注入到网页中 。首选和默认值是 "document_idle"
。
可能的值有:
document_start
:在任何来自 css
的文件之后,但在构建任何其他 DOM 或运行任何其他脚本之前。
document_end
:在 DOM 完成后立即,但在图像和框架等子资源加载之前。
通过 manifest.json
Copy {
"name" : "My extension" ,
...
"content_scripts" : [
{
"matches" : [ "https://*.example.com/*" ] ,
"run_at" : "document_idle" ,
"js" : [ "contentScript.js" ]
}
] ,
...
}
通过 service-worker.js
Copy chrome . scripting .registerContentScripts ([{
id : "test" ,
matches : [ "https://*.example.com/*" ] ,
runAt : "document_idle" ,
js : [ "contentScript.js" ] ,
}]);
background
由内容脚本发送的消息由背景页面 接收,背景页面在协调扩展组件中发挥着核心作用。值得注意的是,背景页面在扩展的整个生命周期中持续存在,默默运行而无需直接用户交互。它拥有自己的文档对象模型(DOM),能够实现复杂的交互和状态管理。
关键点 :
背景页面角色: 作为扩展的神经中枢,确保扩展各部分之间的通信和协调。
持久性: 它是一个始终存在的实体,对用户不可见,但对扩展的功能至关重要。
自动生成: 如果未明确定义,浏览器将自动创建一个背景页面。这个自动生成的页面将包含扩展清单中指定的所有背景脚本,确保扩展的后台任务无缝运行。
浏览器在自动生成背景页面(当未明确声明时)所提供的便利,确保所有必要的背景脚本都被集成并正常运行,从而简化了扩展的设置过程。
示例背景脚本:
Copy chrome . runtime . onMessage .addListener ((request , sender , sendResponse) =>
{
if (request == "explain" )
{
chrome . tabs .create ({ url : "https://example.net/explanation" });
}
})
它使用 runtime.onMessage API 来监听消息。当接收到 "explain"
消息时,它使用 tabs API 在新标签页中打开一个页面。
要调试后台脚本,您可以转到 扩展详细信息并检查服务工作者, 这将打开带有后台脚本的开发者工具:
选项页面和其他
浏览器扩展可以包含各种类型的页面:
选项页面 :此页面在点击时显示在扩展顶部。在之前的清单中,我能够通过 chrome://extensions/?options=fadlhnelkbeojnebcbkacjilhnbjfjca
访问此页面或点击:
请注意,这些页面不像后台页面那样持久,因为它们根据需要动态加载内容。尽管如此,它们与后台页面共享某些功能:
与内容脚本的通信: 类似于后台页面,这些页面可以接收来自内容脚本的消息,促进扩展内的交互。
访问扩展特定的 API: 这些页面享有对扩展特定 API 的全面访问,受扩展定义的权限限制。
permissions
& host_permissions
permissions
和 host_permissions
是 manifest.json
中的条目,指示 浏览器扩展具有哪些权限 (存储、位置等)以及 在哪些网页上 。
由于浏览器扩展可能具有如此 特权 ,恶意扩展或被攻击的扩展可能允许攻击者 以不同方式窃取敏感信息并监视用户 。
检查这些设置如何工作以及它们如何被滥用:
BrowExt - permissions & host_permissions content_security_policy
内容安全策略 也可以在 manifest.json
中声明。如果定义了内容安全策略,它可能是 脆弱的 。
浏览器扩展页面的默认设置相当严格:
Copy script-src 'self' ; object-src 'self' ;
有关CSP和潜在绕过的更多信息,请查看:
Content Security Policy (CSP) Bypass web_accessible_resources
为了让网页访问浏览器扩展的页面,例如一个.html
页面,该页面需要在manifest.json
的**web_accessible_resources
**字段中提及。
例如:
Copy {
...
"web_accessible_resources" : [
{
"resources" : [ "images/*.png" ] ,
"matches" : [ "https://example.com/*" ]
} ,
{
"resources" : [ "fonts/*.woff" ] ,
"matches" : [ "https://example.com/*" ]
}
] ,
...
}
这些页面可以通过以下URL访问:
Copy chrome-extension://<extension-id>/message.html
在公共扩展中,extension-id 是可访问的 :
不过,如果使用 manifest.json
参数 use_dynamic_url
,则该 id 可能是动态的 。
请注意,即使此处提到某个页面,它也可能由于 内容安全策略 而 受到 ClickJacking 保护 。因此,在确认 ClickJacking 攻击是否可能之前,您还需要检查它(frame-ancestors 部分)。
允许访问这些页面使这些页面 可能容易受到 ClickJacking 攻击 :
BrowExt - ClickJacking 仅允许这些页面由扩展加载,而不是由随机 URL 加载,可以防止 ClickJacking 攻击。
请注意,web_accessible_resources
中的页面和扩展的其他页面也能够 联系后台脚本 。因此,如果这些页面中的一个容易受到 XSS 攻击,它可能会打开更大的漏洞。
此外,请注意,您只能在 iframe 中打开 web_accessible_resources
中指示的页面,但从新标签页可以访问扩展中的任何页面,只需知道扩展 ID。因此,如果发现 XSS 利用相同的参数,即使页面未在 web_accessible_resources
中配置,也可能被利用。
externally_connectable
根据 文档 ,"externally_connectable"
清单属性声明 哪些扩展和网页可以通过 runtime.connect 和 runtime.sendMessage 连接到您的扩展。
如果在扩展的清单中 未声明 externally_connectable
键或声明为 "ids": ["*"]
,所有扩展都可以连接,但没有网页可以连接 。
如果指定了 特定 ID ,如 "ids": ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
,只有这些应用程序 可以连接。
Copy "matches" : [
"https://*.google.com/*" ,
"*://*.chromium.org/*" ,
如果指定为空:"externally_connectable": {}
,则没有应用程序或网页能够连接。
这里指示的扩展和URL越少 ,攻击面就越小 。
如果在**externally_connectable
中指示了 易受XSS或接管攻击的网页**,攻击者将能够直接向后台脚本发送消息 ,完全绕过内容脚本及其CSP。
因此,这是一个非常强大的绕过 。
此外,如果客户端安装了一个恶意扩展,即使它不被允许与易受攻击的扩展通信,它也可以在允许的网页中注入XSS数据 ,或滥用**WebRequest
或 DeclarativeNetRequest
** API来操纵目标域上的请求,改变页面对JavaScript文件 的请求。(请注意,目标页面上的CSP可能会阻止这些攻击)。这个想法来自这篇文章 。
通信总结
扩展 <--> WebApp
在内容脚本和网页之间进行通信时,通常使用后续消息。因此,在Web应用程序中,您通常会找到对函数**window.postMessage
的调用,而在内容脚本中则有像 window.addEventListener
这样的监听器。然而,请注意,扩展也可以 通过发送Post Message与Web应用程序通信**(因此网页应该预期到这一点),或者只是让网页加载一个新脚本。
在扩展内部
通常使用函数**chrome.runtime.sendMessage
在扩展内部发送消息(通常由 background
脚本处理),为了接收和处理它,声明一个监听器调用 chrome.runtime.onMessage.addListener
**。
也可以使用**chrome.runtime.connect()
来建立持久连接,而不是发送单个消息,可以像以下示例一样 发送和 接收****消息**:
chrome.runtime.connect()
示例```javascript var port = chrome.runtime.connect();
// Listen for messages from the web page window.addEventListener("message", (event) => { // Only accept messages from the same window if (event.source !== window) { return; }
// Check if the message type is "FROM_PAGE" if (event.data.type && (event.data.type === "FROM_PAGE")) { console.log("Content script received: " + event.data.text); // Forward the message to the background script port.postMessage({ type: 'FROM_PAGE', text: event.data.text }); } }, false);
// Listen for messages from the background script port.onMessage.addListener(function(msg) { console.log("Content script received message from background script:", msg); // Handle the response message from the background script });
Copy </details>
也可以从后台脚本向位于特定标签页中的内容脚本发送消息,调用 **`chrome.tabs.sendMessage`**,在这里你需要指明要发送消息的 **标签页 ID**。
### 从允许的 `externally_connectable` 到扩展
**在 `externally_connectable` 配置中允许的 Web 应用和外部浏览器扩展** 可以使用以下方式发送请求:
```javascript
chrome.runtime.sendMessage(extensionId, ...
需要提到扩展 ID 的地方。
本地消息传递
后台脚本可以与系统内的二进制文件进行通信,如果这种通信没有得到妥善保护,可能会容易受到诸如 RCE 等关键漏洞的影响 。稍后会详细介绍 。
Copy chrome . runtime .sendNativeMessage (
'com.my_company.my_application' ,
{text : 'Hello' } ,
function (response) {
console .log ( 'Received ' + response);
}
);
Web ↔︎ 内容脚本通信
内容脚本 操作的环境与主机页面存在的环境是 分开的 ,确保了 隔离 。尽管存在这种隔离,双方都能够与页面的 文档对象模型 (DOM) 进行交互,这是一个共享资源。为了使主机页面能够与 内容脚本 进行通信,或通过内容脚本间接与扩展进行通信,必须利用双方都可以访问的 DOM 作为通信通道。
发送消息
Copy // This is like "chrome.runtime.sendMessage" but to maintain the connection
var port = chrome . runtime .connect ();
window .addEventListener ( "message" , (event) => {
// We only accept messages from ourselves
if ( event .source !== window) {
return ;
}
if ( event . data .type && ( event . data .type === "FROM_PAGE" )) {
console .log ( "Content script received: " + event . data .text);
// Forward the message to the background script
port .postMessage ( event . data .text);
}
} , false );
Copy document .getElementById ( "theButton" ) .addEventListener ( "click" , () => {
window .postMessage (
{type : "FROM_PAGE" , text : "Hello from the webpage!" } , "*" );
} , false );
安全的 Post Message 通信应检查接收到的消息的真实性,这可以通过以下方式进行检查:
event.isTrusted
:只有在事件由用户操作触发时,这个值才为 True
内容脚本可能只在用户执行某些操作时才期待接收到消息
origin domain :可能只允许白名单中的域名接收消息。
Source :received_message.source !== window
可用于检查消息是否来自 同一窗口 ,即内容脚本正在监听的窗口。
即使执行了上述检查,仍可能存在漏洞,因此请查看以下页面 潜在的 Post Message 绕过 :
Iframe
另一种可能的通信方式可能是通过 Iframe URLs ,您可以在以下位置找到示例:
DOM
这并不是“确切的”通信方式,但 网页和内容脚本将可以访问网页 DOM 。因此,如果 内容脚本 从中读取某些信息,信任网页 DOM ,网页可能会 修改这些数据 (因为网页不应被信任,或者因为网页易受 XSS 攻击)并 危害内容脚本 。
您还可以在以下位置找到一个 基于 DOM 的 XSS 以危害浏览器扩展 的示例:
内容脚本 ↔︎ 后台脚本通信
内容脚本可以使用 runtime.sendMessage() 或 tabs.sendMessage() 发送 一次性 JSON 可序列化 消息。
要处理 响应 ,请使用返回的 Promise 。尽管为了向后兼容,您仍然可以将 回调 作为最后一个参数传递。
从 内容脚本 发送请求的方式如下:
Copy ( async () => {
const response = await chrome . runtime .sendMessage ({greeting : "hello" });
// do something with response here, not outside the function
console .log