From a2567d6e24ced9abfc815f689c434c12928bc637 Mon Sep 17 00:00:00 2001 From: "trop[bot]" <37223003+trop[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 07:35:25 -0500 Subject: [PATCH] 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 --- filenames.gni | 2 + .../extensions/api/scripting/scripting_api.cc | 3 +- shell/browser/extensions/api/tabs/tabs_api.cc | 19 +++--- .../extensions/electron_extension_tab_util.cc | 23 ++++++++ .../extensions/electron_extension_tab_util.h | 29 +++++++++ .../extensions/electron_messaging_delegate.cc | 3 +- spec/extensions-spec.ts | 59 +++++++++++++++++++ .../tabs-cross-session/background.js | 45 ++++++++++++++ .../extensions/tabs-cross-session/main.js | 7 +++ .../tabs-cross-session/manifest.json | 17 ++++++ 10 files changed, 196 insertions(+), 11 deletions(-) create mode 100644 shell/browser/extensions/electron_extension_tab_util.cc create mode 100644 shell/browser/extensions/electron_extension_tab_util.h create mode 100644 spec/fixtures/extensions/tabs-cross-session/background.js create mode 100644 spec/fixtures/extensions/tabs-cross-session/main.js create mode 100644 spec/fixtures/extensions/tabs-cross-session/manifest.json diff --git a/filenames.gni b/filenames.gni index 9e58454614..5afcb40880 100644 --- a/filenames.gni +++ b/filenames.gni @@ -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", diff --git a/shell/browser/extensions/api/scripting/scripting_api.cc b/shell/browser/extensions/api/scripting/scripting_api.cc index 0ec68e2961..d6b82e4793 100644 --- a/shell/browser/extensions/api/scripting/scripting_api.cc +++ b/shell/browser/extensions/api/scripting/scripting_api.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* 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; diff --git a/shell/browser/extensions/api/tabs/tabs_api.cc b/shell/browser/extensions/api/tabs/tabs_api.cc index 6c01e31cfb..b0af07ef0f 100644 --- a/shell/browser/extensions/api/tabs/tabs_api.cc +++ b/shell/browser/extensions/api/tabs/tabs_api.cc @@ -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")); diff --git a/shell/browser/extensions/electron_extension_tab_util.cc b/shell/browser/extensions/electron_extension_tab_util.cc new file mode 100644 index 0000000000..d83334cf87 --- /dev/null +++ b/shell/browser/extensions/electron_extension_tab_util.cc @@ -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 diff --git a/shell/browser/extensions/electron_extension_tab_util.h b/shell/browser/extensions/electron_extension_tab_util.h new file mode 100644 index 0000000000..bf7dd957f0 --- /dev/null +++ b/shell/browser/extensions/electron_extension_tab_util.h @@ -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_ diff --git a/shell/browser/extensions/electron_messaging_delegate.cc b/shell/browser/extensions/electron_messaging_delegate.cc index d0cf15eec1..74c6d6cba7 100644 --- a/shell/browser/extensions/electron_messaging_delegate.cc +++ b/shell/browser/extensions/electron_messaging_delegate.cc @@ -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 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; } diff --git a/spec/extensions-spec.ts b/spec/extensions-spec.ts index 08d2a5a25b..3133c1d057 100644 --- a/spec/extensions-spec.ts +++ b/spec/extensions-spec.ts @@ -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', () => { diff --git a/spec/fixtures/extensions/tabs-cross-session/background.js b/spec/fixtures/extensions/tabs-cross-session/background.js new file mode 100644 index 0000000000..979a0f6cc4 --- /dev/null +++ b/spec/fixtures/extensions/tabs-cross-session/background.js @@ -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; +}); diff --git a/spec/fixtures/extensions/tabs-cross-session/main.js b/spec/fixtures/extensions/tabs-cross-session/main.js new file mode 100644 index 0000000000..680229198a --- /dev/null +++ b/spec/fixtures/extensions/tabs-cross-session/main.js @@ -0,0 +1,7 @@ +/* global chrome */ + +window.addEventListener('message', (event) => { + chrome.runtime.sendMessage(JSON.parse(event.data), (response) => { + console.log(JSON.stringify(response)); + }); +}, false); diff --git a/spec/fixtures/extensions/tabs-cross-session/manifest.json b/spec/fixtures/extensions/tabs-cross-session/manifest.json new file mode 100644 index 0000000000..289d813e57 --- /dev/null +++ b/spec/fixtures/extensions/tabs-cross-session/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "tabs-cross-session", + "version": "1.0", + "manifest_version": 3, + "permissions": ["tabs", "scripting"], + "host_permissions": [""], + "content_scripts": [ + { + "matches": [""], + "js": ["main.js"], + "run_at": "document_start" + } + ], + "background": { + "service_worker": "background.js" + } +}