Chrome · Chromium
CVE-2026-78947
Logic Error in Chromium
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
forfront_end/panels/common/ExtensionServer.ts |
modified | |
iffront_end/panels/common/ExtensionServer.ts |
modified |
Files Changed
front_end/models/extensions/ExtensionAPI.tsfront_end/panels/common/ExtensionServer.test.tsfront_end/panels/common/ExtensionServer.ts
Patch
From 1ce5a8edfc98998f890b8003855d72a569b18379 Mon Sep 17 00:00:00 2001 From: David Garber <[email protected]> Date: Mon, 06 Jul 2026 18:19:34 +0000 Subject: [PATCH] Clear extension headers between domains and when extensions are disabled New unit tests confirm undesired access (i.e. they fail), prior to fix, and then validate fix. Bug: 523313378 Change-Id: Ic31e3cdc333c96cfeb941635813902f401205b32 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7934690 Reviewed-by: Danil Somsikov <[email protected]> Commit-Queue: David Garber <[email protected]> Reviewed-by: Philip Pfaffe <[email protected]> --- diff --git a/front_end/models/extensions/ExtensionAPI.ts b/front_end/models/extensions/ExtensionAPI.ts index 4a64766..58f393b 100644 --- a/front_end/models/extensions/ExtensionAPI.ts +++ b/front_end/models/extensions/ExtensionAPI.ts @@ -374,6 +374,10 @@ } export type RecorderExtensionRequests = StringifyRequest|StringifyStepRequest|ReplayRequest; + + export interface Network extends PublicAPI.Chrome.DevTools.Network { + addRequestHeaders(headers: Record<string, string>): void; + } } declare global { @@ -438,10 +442,6 @@ _dispatch(request: {arguments: unknown[]}): void; } - export interface Network extends PublicAPI.Chrome.DevTools.Network { - addRequestHeaders(headers: Record<string, string>): void; - } - export interface Request extends PublicAPI.Chrome.DevTools.Request, HAR.Log.EntryDTO { _id: number; } @@ -589,7 +589,7 @@ defineDeprecatedProperty(this, 'webInspector', 'resources', 'network'); } - function Network(this: APIImpl.Network): void { + function Network(this: PrivateAPI.Network): void { function dispatchRequestEvent( this: APIImpl.EventSink<(request: PublicAPI.Chrome.DevTools.Request) => unknown>, message: {arguments: unknown[]}): void { @@ -606,7 +606,7 @@ this.onNavigated = new (Constructor(EventSink))(PrivateAPI.Events.InspectedURLChanged); } - (Network.prototype as Pick<APIImpl.Network, 'getHAR'|'addRequestHeaders'>) = { + (Network.prototype as Pick<PrivateAPI.Network, 'getHAR'|'addRequestHeaders'>) = { getHAR: function(this: PublicAPI.Chrome.DevTools.Network, _callback?: (harLog: object) => unknown): Promise<object>| void { const {callback: callbackArg, promise, resolve, reject} = callbackOrPromise<object>(arguments); diff --git a/front_end/panels/common/ExtensionServer.test.ts b/front_end/panels/common/ExtensionServer.test.ts index 1c34469..0bffc51 100644 --- a/front_end/panels/common/ExtensionServer.test.ts +++ b/front_end/panels/common/ExtensionServer.test.ts @@ -76,8 +76,7 @@ const headersCall = spyCall(SDK.NetworkManager.MultitargetNetworkManager.instance(), 'setExtraHTTPHeaders'); - const networkApi = - context.chrome.devtools?.network as unknown as {addRequestHeaders(headers: Record<string, string>): void}; + const networkApi = context.chrome.devtools?.network as Extensions.ExtensionAPI.PrivateAPI.Network; networkApi.addRequestHeaders({'X-Test': 'v'}); const {args} = await headersCall; @@ -1294,10 +1293,9 @@ const setHeadersSpy = sinon.spy(SDK.NetworkManager.MultitargetNetworkManager.instance(), 'setExtraHTTPHeaders'); - const networkApi = - context.chrome.devtools?.network as unknown as {addRequestHeaders(headers: Record<string, string>): void}; + const networkApi = context.chrome.devtools?.network as Extensions.ExtensionAPI.PrivateAPI.Network; networkApi.addRequestHeaders({'X-Test': '1'}); - // Round-trip a callback command on the same MessagePort to ensure the + // Round-trip a command on the same MessagePort to ensure the // addRequestHeaders message has been processed before we assert. await context.chrome.devtools!.network.getHAR(); @@ -1305,6 +1303,78 @@ }); }); +describe('addRequestHeaders security', () => { + const context = setupDevtoolsExtensionHooks(); + // Helper: sets headers on a permitted page, navigates to the given URL, then + // manually triggers modelAdded on a new target and verifies that the injected + // headers are NOT applied via CDP. + async function assertHeadersNotAppliedAfterNavigation( + navigateToUrl: Platform.DevToolsPath.UrlString, + injectedHeaders: Record<string, string>, + ): Promise<void> { + const target = createTarget({type: SDK.Target.Type.FRAME}); + target.setInspectedURL(urlString`http://example.com`); + assert.exists(context.chrome.devtools); + + const multitargetManager = SDK.NetworkManager.MultitargetNetworkManager.instance(); + + // Set headers while on a permitted page. + const networkApi = context.chrome.devtools?.network as Extensions.ExtensionAPI.PrivateAPI.Network; + networkApi.addRequestHeaders(injectedHeaders); + await context.chrome.devtools?.network.getHAR(() => {}); + + // Navigate to a URL where the extension should NOT have access. + target.setInspectedURL(navigateToUrl); + + // Simulate a new target attaching (e.g., OOPIF or service worker). + // Set up the spy before manually calling modelAdded so we capture exactly + // what headers get pushed via CDP. + const newTarget = createTarget({type: SDK.Target.Type.FRAME, parentTarget: target}); + const networkAgent = newTarget.networkAgent(); + const cdpSpy = sinon.spy(networkAgent, 'invoke_setExtraHTTPHeaders'); + const networkManager = newTarget.model(SDK.NetworkManager.NetworkManager); + assert.exists(cdpSpy); + assert.exists(networkManager); + assert.exists(multitargetManager); + multitargetManager.modelAdded(networkManager); + + // Confirm invoke_setExtraHTTPHeaders was called by modelAdded. + sinon.assert.called(cdpSpy); + const appliedHeaders = cdpSpy.lastCall.args[0].headers; + for (const key of Object.keys(injectedHeaders)) { + assert.notProperty(appliedHeaders, key, + `Header "${key}" was applied to a target on ${navigateToUrl} — ` + + `extension-set headers persisted across navigation to a disallowed URL`); + } + } + + it('extension-injected headers must not leak to chrome:// targets after navigation', async () => { + await assertHeadersNotAppliedAfterNavigation( + urlString`chrome://settings`, + {Cookie: 'session=attacker', 'X-CSRF-Token': 'injected'}, + ); + }); + + it('extension-injected headers must not leak to forbidden-origin targets after navigation', async () => { + // Simulate getOriginsForbiddenForExtensions returning a forbidden origin. + window.DevToolsAPI = { + getOriginsForbiddenForExtensions: () => ['https://addons.example.com'], + }; + + await assertHeadersNotAppliedAfterNavigation( + urlString`https://addons.example.com/extensions`, + {Authorization: 'Bearer attacker'}, + ); + }); + + it('extension-injected headers must not leak to file:// targets without file access', async () => { + await assertHeadersNotAppliedAfterNavigation( + urlString`file:///etc/passwd`, + {'X-Injected': 'value'}, + ); + }); +}); + describe('ExtensionServer', () => { it('can correctly expand resource paths', async () => { // Ideally this would be a chrome-extension://, but that doesn't work with URL in chrome headless. diff --git a/front_end/panels/common/ExtensionServer.ts b/front_end/panels/common/ExtensionServer.ts index a8dab9c..110d904 100644 --- a/front_end/panels/common/ExtensionServer.ts +++ b/front_end/panels/common/ExtensionServer.ts @@ -564,6 +564,7 @@ return; } this.requests = new Map(); + this.clearExtensionHeaders(event.data.inspectedURL()); this.enableExtensions(); const url = event.data.inspectedURL(); this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.InspectedURLChanged, [url]); @@ -665,16 +666,7 @@ for (const name in message.headers) { extensionHeaders.set(name, message.headers[name]); } - const allHeaders = ({} as Protocol.Network.Headers); - for (const headers of this.extraHeaders.values()) { - for (const [name, value] of headers) { - if (name !== '__proto__' && typeof value === 'string') { - allHeaders[name] = value; - } - } - } - - SDK.NetworkManager.MultitargetNetworkManager.instance().setExtraHTTPHeaders(allHeaders); + this.syncExtraHeaders(); return undefined; } @@ -1787,11 +1779,56 @@ private disableExtensions(): void { this.extensionsEnabled = false;
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page