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 <[email protected]> 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 <[email protected]> Reviewed-by: Philip Pfaffe <[email protected]> Reviewed-by: Simon Zünd <[email protected]> --- 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