mirror of
https://github.com/electron/electron.git
synced 2026-05-02 03:00:22 -04:00
fix: scope extension tab-ID resolution to the calling BrowserContext (#50926)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Sam Attard <sattard@anthropic.com>
This commit is contained in:
@@ -780,6 +780,8 @@ filenames = {
|
||||
"shell/browser/extensions/electron_extension_system_factory.h",
|
||||
"shell/browser/extensions/electron_extension_system.cc",
|
||||
"shell/browser/extensions/electron_extension_system.h",
|
||||
"shell/browser/extensions/electron_extension_tab_util.cc",
|
||||
"shell/browser/extensions/electron_extension_tab_util.h",
|
||||
"shell/browser/extensions/electron_extension_web_contents_observer.cc",
|
||||
"shell/browser/extensions/electron_extension_web_contents_observer.h",
|
||||
"shell/browser/extensions/electron_extensions_api_client.cc",
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#include "extensions/common/utils/content_script_utils.h"
|
||||
#include "extensions/common/utils/extension_types_utils.h"
|
||||
#include "shell/browser/api/electron_api_web_contents.h"
|
||||
#include "shell/browser/extensions/electron_extension_tab_util.h"
|
||||
#include "third_party/abseil-cpp/absl/strings/str_format.h"
|
||||
|
||||
namespace extensions {
|
||||
@@ -270,7 +271,7 @@ bool CanAccessTarget(const PermissionsData& permissions,
|
||||
ScriptExecutor::FrameScope* frame_scope_out,
|
||||
std::set<int>* frame_ids_out,
|
||||
std::string* error_out) {
|
||||
auto* contents = electron::api::WebContents::FromID(target.tab_id);
|
||||
auto* contents = GetElectronTabById(target.tab_id, browser_context);
|
||||
if (!contents) {
|
||||
*error_out = absl::StrFormat("No tab with id: %d", target.tab_id);
|
||||
return false;
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "extensions/common/permissions/permissions_data.h"
|
||||
#include "extensions/common/switches.h"
|
||||
#include "shell/browser/api/electron_api_web_contents.h"
|
||||
#include "shell/browser/extensions/electron_extension_tab_util.h"
|
||||
#include "shell/browser/native_window.h"
|
||||
#include "shell/browser/web_contents_zoom_controller.h"
|
||||
#include "shell/browser/window_list.h"
|
||||
@@ -138,7 +139,7 @@ bool ExecuteCodeInTabFunction::CanExecuteScriptOnPage(std::string* error) {
|
||||
// If |tab_id| is specified, look for the tab. Otherwise default to selected
|
||||
// tab in the current window.
|
||||
CHECK_GE(execute_tab_id_, 0);
|
||||
auto* contents = electron::api::WebContents::FromID(execute_tab_id_);
|
||||
auto* contents = GetElectronTabById(execute_tab_id_, browser_context());
|
||||
if (!contents) {
|
||||
return false;
|
||||
}
|
||||
@@ -191,7 +192,7 @@ bool ExecuteCodeInTabFunction::CanExecuteScriptOnPage(std::string* error) {
|
||||
|
||||
ScriptExecutor* ExecuteCodeInTabFunction::GetScriptExecutor(
|
||||
std::string* error) {
|
||||
auto* contents = electron::api::WebContents::FromID(execute_tab_id_);
|
||||
auto* contents = GetElectronTabById(execute_tab_id_, browser_context());
|
||||
if (!contents)
|
||||
return nullptr;
|
||||
return contents->script_executor();
|
||||
@@ -228,7 +229,7 @@ ExtensionFunction::ResponseAction TabsReloadFunction::Run() {
|
||||
}
|
||||
|
||||
int tab_id = params->tab_id ? *params->tab_id : -1;
|
||||
auto* contents = electron::api::WebContents::FromID(tab_id);
|
||||
auto* contents = GetElectronTabById(tab_id, browser_context());
|
||||
if (!contents)
|
||||
return RespondNow(Error("No such tab"));
|
||||
|
||||
@@ -335,7 +336,7 @@ ExtensionFunction::ResponseAction TabsGetFunction::Run() {
|
||||
EXTENSION_FUNCTION_VALIDATE(params);
|
||||
int tab_id = params->tab_id;
|
||||
|
||||
auto* contents = electron::api::WebContents::FromID(tab_id);
|
||||
auto* contents = GetElectronTabById(tab_id, browser_context());
|
||||
if (!contents)
|
||||
return RespondNow(Error("No such tab"));
|
||||
|
||||
@@ -367,7 +368,7 @@ ExtensionFunction::ResponseAction TabsSetZoomFunction::Run() {
|
||||
EXTENSION_FUNCTION_VALIDATE(params);
|
||||
|
||||
int tab_id = params->tab_id ? *params->tab_id : -1;
|
||||
auto* contents = electron::api::WebContents::FromID(tab_id);
|
||||
auto* contents = GetElectronTabById(tab_id, browser_context());
|
||||
if (!contents)
|
||||
return RespondNow(Error("No such tab"));
|
||||
|
||||
@@ -394,7 +395,7 @@ ExtensionFunction::ResponseAction TabsGetZoomFunction::Run() {
|
||||
EXTENSION_FUNCTION_VALIDATE(params);
|
||||
|
||||
int tab_id = params->tab_id ? *params->tab_id : -1;
|
||||
auto* contents = electron::api::WebContents::FromID(tab_id);
|
||||
auto* contents = GetElectronTabById(tab_id, browser_context());
|
||||
if (!contents)
|
||||
return RespondNow(Error("No such tab"));
|
||||
|
||||
@@ -410,7 +411,7 @@ ExtensionFunction::ResponseAction TabsGetZoomSettingsFunction::Run() {
|
||||
EXTENSION_FUNCTION_VALIDATE(params);
|
||||
|
||||
int tab_id = params->tab_id ? *params->tab_id : -1;
|
||||
auto* contents = electron::api::WebContents::FromID(tab_id);
|
||||
auto* contents = GetElectronTabById(tab_id, browser_context());
|
||||
if (!contents)
|
||||
return RespondNow(Error("No such tab"));
|
||||
|
||||
@@ -434,7 +435,7 @@ ExtensionFunction::ResponseAction TabsSetZoomSettingsFunction::Run() {
|
||||
EXTENSION_FUNCTION_VALIDATE(params);
|
||||
|
||||
int tab_id = params->tab_id ? *params->tab_id : -1;
|
||||
auto* contents = electron::api::WebContents::FromID(tab_id);
|
||||
auto* contents = GetElectronTabById(tab_id, browser_context());
|
||||
if (!contents)
|
||||
return RespondNow(Error("No such tab"));
|
||||
|
||||
@@ -610,7 +611,7 @@ ExtensionFunction::ResponseAction TabsUpdateFunction::Run() {
|
||||
EXTENSION_FUNCTION_VALIDATE(params);
|
||||
|
||||
int tab_id = params->tab_id ? *params->tab_id : -1;
|
||||
auto* contents = electron::api::WebContents::FromID(tab_id);
|
||||
auto* contents = GetElectronTabById(tab_id, browser_context());
|
||||
if (!contents)
|
||||
return RespondNow(Error("No such tab"));
|
||||
|
||||
|
||||
23
shell/browser/extensions/electron_extension_tab_util.cc
Normal file
23
shell/browser/extensions/electron_extension_tab_util.cc
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2026 Anthropic PBC
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/extensions/electron_extension_tab_util.h"
|
||||
|
||||
#include "content/public/browser/web_contents.h"
|
||||
#include "shell/browser/api/electron_api_web_contents.h"
|
||||
|
||||
namespace extensions {
|
||||
|
||||
electron::api::WebContents* GetElectronTabById(
|
||||
int tab_id,
|
||||
content::BrowserContext* browser_context) {
|
||||
auto* contents = electron::api::WebContents::FromID(tab_id);
|
||||
if (!contents || !contents->web_contents())
|
||||
return nullptr;
|
||||
if (contents->web_contents()->GetBrowserContext() != browser_context)
|
||||
return nullptr;
|
||||
return contents;
|
||||
}
|
||||
|
||||
} // namespace extensions
|
||||
29
shell/browser/extensions/electron_extension_tab_util.h
Normal file
29
shell/browser/extensions/electron_extension_tab_util.h
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2026 Anthropic PBC
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef ELECTRON_SHELL_BROWSER_EXTENSIONS_ELECTRON_EXTENSION_TAB_UTIL_H_
|
||||
#define ELECTRON_SHELL_BROWSER_EXTENSIONS_ELECTRON_EXTENSION_TAB_UTIL_H_
|
||||
|
||||
namespace content {
|
||||
class BrowserContext;
|
||||
} // namespace content
|
||||
|
||||
namespace electron::api {
|
||||
class WebContents;
|
||||
} // namespace electron::api
|
||||
|
||||
namespace extensions {
|
||||
|
||||
// Resolves |tab_id| to an electron::api::WebContents only if the underlying
|
||||
// WebContents belongs to |browser_context|. Tabs in other BrowserContexts are
|
||||
// treated as nonexistent so that an extension loaded into one Session cannot
|
||||
// observe or operate on windows belonging to another Session, matching
|
||||
// Chrome's profile-scoped ExtensionTabUtil::GetTabById semantics.
|
||||
electron::api::WebContents* GetElectronTabById(
|
||||
int tab_id,
|
||||
content::BrowserContext* browser_context);
|
||||
|
||||
} // namespace extensions
|
||||
|
||||
#endif // ELECTRON_SHELL_BROWSER_EXTENSIONS_ELECTRON_EXTENSION_TAB_UTIL_H_
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "extensions/common/api/messaging/port_id.h"
|
||||
#include "extensions/common/extension.h"
|
||||
#include "shell/browser/api/electron_api_web_contents.h"
|
||||
#include "shell/browser/extensions/electron_extension_tab_util.h"
|
||||
#include "ui/gfx/native_ui_types.h"
|
||||
#include "url/gurl.h"
|
||||
|
||||
@@ -58,7 +59,7 @@ std::optional<base::DictValue> ElectronMessagingDelegate::MaybeGetTabInfo(
|
||||
content::WebContents* ElectronMessagingDelegate::GetWebContentsByTabId(
|
||||
content::BrowserContext* browser_context,
|
||||
int tab_id) {
|
||||
auto* contents = electron::api::WebContents::FromID(tab_id);
|
||||
auto* contents = GetElectronTabById(tab_id, browser_context);
|
||||
if (!contents) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -1338,6 +1338,65 @@ describe('chrome extensions', () => {
|
||||
expect(bgAfter).to.equal('rgb(255, 0, 0)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cross-session isolation', () => {
|
||||
let extSession: Session;
|
||||
let otherSession: Session;
|
||||
let driver: BrowserWindow;
|
||||
let victim: BrowserWindow;
|
||||
|
||||
before(async () => {
|
||||
extSession = session.fromPartition(`persist:${uuid.v4()}`);
|
||||
otherSession = session.fromPartition(`persist:${uuid.v4()}`);
|
||||
await extSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'tabs-cross-session'));
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
driver = new BrowserWindow({ show: false, webPreferences: { session: extSession } });
|
||||
victim = new BrowserWindow({ show: false, webPreferences: { session: otherSession } });
|
||||
await driver.loadURL(url);
|
||||
await victim.loadURL(url);
|
||||
});
|
||||
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
const callExtension = async (method: string, tabId: number, args: any[] = []) => {
|
||||
const message = JSON.stringify({ method, tabId, args });
|
||||
const p = once(driver.webContents, 'console-message');
|
||||
await driver.webContents.executeJavaScript(`window.postMessage('${message}', '*')`);
|
||||
const [{ message: responseString }] = await p;
|
||||
return JSON.parse(responseString);
|
||||
};
|
||||
|
||||
it('chrome.tabs.get cannot resolve a tab from another session', async () => {
|
||||
const sameSession = await callExtension('get', driver.webContents.id);
|
||||
expect(sameSession.ok).to.be.true();
|
||||
expect(sameSession.result.id).to.equal(driver.webContents.id);
|
||||
|
||||
const crossSession = await callExtension('get', victim.webContents.id);
|
||||
expect(crossSession.ok).to.be.false();
|
||||
expect(crossSession.error).to.match(/No such tab|No tab with id/);
|
||||
});
|
||||
|
||||
it('chrome.tabs.update cannot navigate a tab in another session', async () => {
|
||||
const before = victim.webContents.getURL();
|
||||
const crossSession = await callExtension('update', victim.webContents.id, [{ url }]);
|
||||
expect(crossSession.ok).to.be.false();
|
||||
expect(crossSession.error).to.match(/No such tab|No tab with id/);
|
||||
expect(victim.webContents.getURL()).to.equal(before);
|
||||
});
|
||||
|
||||
it('chrome.scripting.executeScript cannot target a tab in another session', async () => {
|
||||
const crossSession = await callExtension('executeScript', victim.webContents.id);
|
||||
expect(crossSession.ok).to.be.false();
|
||||
expect(crossSession.error).to.match(/No tab with id/);
|
||||
});
|
||||
|
||||
it('chrome.tabs.sendMessage cannot reach a tab in another session', async () => {
|
||||
const crossSession = await callExtension('sendMessage', victim.webContents.id, ['ping']);
|
||||
expect(crossSession.ok).to.be.false();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom protocol', () => {
|
||||
|
||||
45
spec/fixtures/extensions/tabs-cross-session/background.js
vendored
Normal file
45
spec/fixtures/extensions/tabs-cross-session/background.js
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
/* global chrome */
|
||||
|
||||
const handleRequest = async (request, sender, sendResponse) => {
|
||||
const { method, tabId, args = [] } = request;
|
||||
|
||||
try {
|
||||
switch (method) {
|
||||
case 'get': {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
sendResponse({ ok: true, result: tab });
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
const tab = await chrome.tabs.update(tabId, args[0]);
|
||||
sendResponse({ ok: true, result: tab });
|
||||
break;
|
||||
}
|
||||
case 'reload': {
|
||||
await chrome.tabs.reload(tabId);
|
||||
sendResponse({ ok: true });
|
||||
break;
|
||||
}
|
||||
case 'executeScript': {
|
||||
const results = await chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
func: () => document.title
|
||||
});
|
||||
sendResponse({ ok: true, result: results });
|
||||
break;
|
||||
}
|
||||
case 'sendMessage': {
|
||||
const response = await chrome.tabs.sendMessage(tabId, args[0]);
|
||||
sendResponse({ ok: true, result: response });
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
sendResponse({ ok: false, error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
handleRequest(request, sender, sendResponse);
|
||||
return true;
|
||||
});
|
||||
7
spec/fixtures/extensions/tabs-cross-session/main.js
vendored
Normal file
7
spec/fixtures/extensions/tabs-cross-session/main.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/* global chrome */
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
chrome.runtime.sendMessage(JSON.parse(event.data), (response) => {
|
||||
console.log(JSON.stringify(response));
|
||||
});
|
||||
}, false);
|
||||
17
spec/fixtures/extensions/tabs-cross-session/manifest.json
vendored
Normal file
17
spec/fixtures/extensions/tabs-cross-session/manifest.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "tabs-cross-session",
|
||||
"version": "1.0",
|
||||
"manifest_version": 3,
|
||||
"permissions": ["tabs", "scripting"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["main.js"],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user