Chrome · Extensions
CVE-2026-87431
Logic Error in Extensions
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
iffront_end/panels/common/ExtensionServer.test.ts |
modified |
Files Changed
front_end/models/bindings/CSSWorkspaceBinding.test.tsfront_end/models/bindings/CSSWorkspaceBinding.tsfront_end/models/bindings/CompilerScriptMapping.tsfront_end/models/bindings/DebuggerWorkspaceBinding.tsfront_end/models/bindings/SASSSourceMapping.tsfront_end/panels/common/ExtensionServer.test.ts
Patch
From ecaa2e8404a1903859091b66a93ccf58babf1da6 Mon Sep 17 00:00:00 2001
From: David Garber <dgarber@microsoft.com>
Date: Fri, 07 Aug 2026 15:06:28 -0700
Subject: [PATCH] onAttachSourceMapToResource to throw an error if disallowed sourceMapURL
New unit tests confirm undesired access (i.e. they fail), prior to fix,
and then validate fix.
Bug: 523313374
Change-Id: Ic3f8d44f99067f1de900b94bd629bf7c06cc7e07
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7934689
Commit-Queue: David Garber <dgarber@microsoft.com>
Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
Reviewed-by: Simon Zünd <szuend@chromium.org>
---
diff --git a/front_end/models/bindings/CSSWorkspaceBinding.test.ts b/front_end/models/bindings/CSSWorkspaceBinding.test.ts
index 0343af4..1dc4789 100644
--- a/front_end/models/bindings/CSSWorkspaceBinding.test.ts
+++ b/front_end/models/bindings/CSSWorkspaceBinding.test.ts
@@ -124,6 +124,8 @@
assert.exists(scssUISourceCode);
await cssWorkspaceBinding.pendingLiveLocationChangesPromise();
+ assert.deepEqual(cssWorkspaceBinding.sourceMapURLsForUISourceCode(scssUISourceCode),
+ [urlString`http://example.com/example.css.map`]);
const header = cssModel.styleSheetHeaderForId(headerPayload.styleSheetId);
assert.exists(header);
diff --git a/front_end/models/bindings/CSSWorkspaceBinding.ts b/front_end/models/bindings/CSSWorkspaceBinding.ts
index 1c31529..103a236 100644
--- a/front_end/models/bindings/CSSWorkspaceBinding.ts
+++ b/front_end/models/bindings/CSSWorkspaceBinding.ts
@@ -143,6 +143,10 @@
}
return rawLocations;
}
+
+ sourceMapURLsForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): Platform.DevToolsPath.UrlString[] {
+ return SASSSourceMapping.sourceMapURLsForUISourceCode(uiSourceCode);
+ }
}
export interface SourceMapping {
diff --git a/front_end/models/bindings/CompilerScriptMapping.ts b/front_end/models/bindings/CompilerScriptMapping.ts
index a38439e..cc5b05c 100644
--- a/front_end/models/bindings/CompilerScriptMapping.ts
+++ b/front_end/models/bindings/CompilerScriptMapping.ts
@@ -577,6 +577,10 @@
return scripts;
}
+ sourceMapURLsForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): Platform.DevToolsPath.UrlString[] {
+ return [...this.#uiSourceCodeToSourceMaps.get(uiSourceCode)].map(sourceMap => sourceMap.url());
+ }
+
private sourceMapAttachedForTest(_sourceMap: SDK.SourceMap.SourceMap): void {
}
diff --git a/front_end/models/bindings/DebuggerWorkspaceBinding.ts b/front_end/models/bindings/DebuggerWorkspaceBinding.ts
index 3c73a0b..cef7a5f 100644
--- a/front_end/models/bindings/DebuggerWorkspaceBinding.ts
+++ b/front_end/models/bindings/DebuggerWorkspaceBinding.ts
@@ -463,6 +463,14 @@
return [...scripts];
}
+ sourceMapURLsForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): Platform.DevToolsPath.UrlString[] {
+ const urls = new Set<Platform.DevToolsPath.UrlString>();
+ for (const modelData of this.#debuggerModelToData.values()) {
+ modelData.compilerMapping.sourceMapURLsForUISourceCode(uiSourceCode).forEach(url => urls.add(url));
+ }
+ return [...urls];
+ }
+
supportsConditionalBreakpoints(uiSourceCode: Workspace.UISourceCode.UISourceCode): boolean {
const scripts = this.pluginManager.scriptsForUISourceCode(uiSourceCode);
return scripts.every(script => script.isJavaScript());
diff --git a/front_end/models/bindings/SASSSourceMapping.ts b/front_end/models/bindings/SASSSourceMapping.ts
index fe192b2..d287d85 100644
--- a/front_end/models/bindings/SASSSourceMapping.ts
+++ b/front_end/models/bindings/SASSSourceMapping.ts
@@ -140,6 +140,12 @@
return [];
}
+ static sourceMapURLsForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode):
+ Platform.DevToolsPath.UrlString[] {
+ const binding = uiSourceCodeToBinding.get(uiSourceCode);
+ return binding?.getReferringSourceMaps().map(sourceMap => sourceMap.url()) ?? [];
+ }
+
dispose(): void {
Common.EventTarget.removeEventListeners(this.#eventListeners);
this.#project.dispose();
diff --git a/front_end/panels/common/ExtensionServer.test.ts b/front_end/panels/common/ExtensionServer.test.ts
index 5ac6417..3927866 100644
--- a/front_end/panels/common/ExtensionServer.test.ts
+++ b/front_end/panels/common/ExtensionServer.test.ts
@@ -923,7 +923,7 @@
sinon.stub(Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding, 'instance')
.returns(sinon.createStubInstance(Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding,
- {scriptsForUISourceCode: []}));
+ {scriptsForUISourceCode: [], sourceMapURLsForUISourceCode: []}));
const project = new Bindings.ContentProviderBasedProject.ContentProviderBasedProject(
Workspace.Workspace.WorkspaceImpl.instance(), target.id(), Workspace.Workspace.projectTypes.Network, '',
false /* isServiceProject */);
@@ -962,7 +962,7 @@
sinon.stub(Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding, 'instance')
.returns(sinon.createStubInstance(Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding,
- {scriptsForUISourceCode: []}));
+ {scriptsForUISourceCode: [], sourceMapURLsForUISourceCode: []}));
const project = new Bindings.ContentProviderBasedProject.ContentProviderBasedProject(
Workspace.Workspace.WorkspaceImpl.instance(), blockedTarget.id(), Workspace.Workspace.projectTypes.Network, '',
@@ -990,6 +990,7 @@
const script = sinon.createStubInstance(SDK.Script.Script, {target, contentURL: blockedUrl});
script.hasSourceURL = true;
const workspaceBinding = sinon.createStubInstance(Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding);
+ workspaceBinding.sourceMapURLsForUISourceCode.returns([]);
workspaceBinding.scriptsForUISourceCode.callsFake(uiSourceCode => {
if (uiSourceCode.contentURL() === blockedUrl) {
return [script];
@@ -1017,6 +1018,7 @@
script.hasSourceURL = true;
script.embedderName.returns(blockedUrl);
const workspaceBinding = sinon.createStubInstance(Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding);
+ workspaceBinding.sourceMapURLsForUISourceCode.returns([]);
workspaceBinding.scriptsForUISourceCode.callsFake(uiSourceCode => {
if (uiSourceCode.contentURL() === allowedUrl) {
return [script];
@@ -1035,6 +1037,24 @@
assert.deepEqual(resources.map(r => r.url), []);
});
+ it('blocks CSS sources originating from a blocked source map URL', async () => {
+ const target = getBackend(context).createTarget({id: 'target' as Protocol.Target.TargetID});
+ target.setInspectedURL(allowedUrl);
+
+ sinon.stub(Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding, 'instance')
+ .returns(sinon.createStubInstance(Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding,
+ {scriptsForUISourceCode: [], sourceMapURLsForUISourceCode: []}));
+ const cssWorkspaceBinding = Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance();
+ (cssWorkspaceBinding.sourceMapURLsForUISourceCode as sinon.SinonStub).returns([blockedUrl]);
+ const project = new Bindings.ContentProviderBasedProject.ContentProviderBasedProject(
+ Workspace.Workspace.WorkspaceImpl.instance(), target.id(), Workspace.Workspace.projectTypes.Network, '',
+ false /* isServiceProject */);
+ await createUISourceCode(project, allowedUrl, Common.ResourceType.resourceTypes.SourceMapStyleSheet);
+
+ const resources = await context.chrome.devtools!.inspectedWindow.getResources();
+ assert.deepEqual(resources.map(resource => resource.url), []);
+ });
+
it('allows scripts with sourceURL comments if the embedderName is not a URL', async () => {
const target = getBackend(context).createTarget({id: 'target' as Protocol.Target.TargetID});
target.setInspectedURL(allowedUrl);
@@ -1043,6 +1063,7 @@
script.hasSourceURL = true;
script.embedderName.returns(urlString`eval`);
const workspaceBinding = sinon.createStubInstance(Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding);
+ workspaceBinding.sourceMapURLsForUISourceCode.returns([]);
workspaceBinding.scriptsForUISourceCode.callsFake(uiSourceCode => {
if (uiSourceCode.contentURL() === allowedUrl) {
return [script];
@@ -1306,7 +1327,7 @@
sinon.stub(Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding, 'instance')
.returns(sinon.createStubInstance(Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding,
- {scriptsForUISourceCode: []}));
+ {scriptsForUISourceCode: [], sourceMapURLsForUISourceCode: []}));
const project = new Bindings.ContentProviderBasedProject.ContentProviderBasedProject(
Workspace.Workspace.WorkspaceImpl.instance(), target.id(), Workspace.Workspace.projectTypes.Network, '',
false /* isServiceProject */);
@@ -1652,6 +1673,47 @@
}
}
+async function createScriptResource(
+ context: ExtensionContext,
+ options?: {
+ scriptUrl?: Platform.DevToolsPath.UrlString,
+ scriptContent?: string,
+ inspectedUrl?: Platform.DevToolsPath.UrlString,
+ },
+ ): Promise<{
+ target: SDK.Target.Target,
+ currentScript: SDK.Script.Script,
+ scriptResource: Chrome.DevTools.Resource,
+}> {
+ const scriptUrl = options?.scriptUrl ?? urlString`http://example.com/script.js`;
+ const scriptContent =
+ options?.scriptContent ?? 'function f(x) { console.log(x); } function ignore(y){ console.log(y); }';
+
+ const backend = getBackend(context);
+ mockResourceTree(backend.cdpConnection);
+ const target = backend.createTarget({type: SDK.Target.Type.FRAME});
+ if (options?.inspectedUrl) {
+ target.setInspectedURL(options.inspectedUrl);
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/front_end/testing/ExtensionHelpers.ts b/front_end/testing/ExtensionHelpers.ts
index dafde42..f52e5aa 100644
--- a/front_end/testing/ExtensionHelpers.ts
+++ b/front_end/testing/ExtensionHelpers.ts
@@ -7,6 +7,7 @@
import type {Chrome} from '../../extension-api/ExtensionAPI.js';
import * as Host from '../core/host/host.js';
import * as SDK from '../core/sdk/sdk.js';
+import * as Bindings from '../models/bindings/bindings.js';
import type * as Extensions from '../models/extensions/extensions.js';
import * as Logs from '../models/logs/logs.js';
import * as Workspace from '../models/workspace/workspace.js';
@@ -78,6 +79,10 @@
context.backend = backend;
sinon.stub(Workspace.Workspace.WorkspaceImpl, 'instance').returns(backend.universe.workspace);
sinon.stub(SDK.TargetManager.TargetManager, 'instance').returns(backend.universe.targetManager);
+ const cssWorkspaceBinding = sinon.createStubInstance(Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding, {
+ sourceMapURLsForUISourceCode: [],
+ });
+ sinon.stub(Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding, 'instance').returns(cssWorkspaceBinding);
const networkLog = new Logs.NetworkLog.NetworkLog(backend.universe.targetManager, backend.universe.settings);
sinon.stub(Logs.NetworkLog.NetworkLog, 'instance').returns(networkLog);
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