fix: scope extension tab-ID resolution to the calling BrowserContext (#50923)

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:
trop[bot]
2026-04-11 07:36:13 -05:00
committed by GitHub
parent 10fb5b39c5
commit a078ed77c5
10 changed files with 196 additions and 11 deletions

View File

@@ -781,6 +781,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",

View File

@@ -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;

View File

@@ -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"));
@@ -408,7 +409,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"));
@@ -432,7 +433,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"));
@@ -608,7 +609,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"));

View 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

View 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_

View File

@@ -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;
}

View File

@@ -1431,6 +1431,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', () => {

View 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;
});

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

View 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"
}
}