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 <dgarber@microsoft.com>
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 <dsv@chromium.org>
Commit-Queue: David Garber <dgarber@microsoft.com>
Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
---
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