Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncomplete cleanup in Chromium
DescriptionIncomplete cleanup in Chromium
ComponentChromium
Bug ClassLogic Error
Tracker523313378
Fix commit1ce5a8edfc98 (devtools/devtools-frontend) +128/-21
CISA KEVNot listed
CreditedMicrosoft Edge
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
for
front_end/panels/common/ExtensionServer.ts
modified
if
front_end/panels/common/ExtensionServer.ts
modified

Files Changed

  • front_end/models/extensions/ExtensionAPI.ts
  • front_end/panels/common/ExtensionServer.test.ts
  • front_end/panels/common/ExtensionServer.ts
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.