CVE-2026-87431
Overview
Background
- DevTools extension
- a third-party extension using the
chrome.devtoolsAPI (here viaExtensionServer) that can enumerate and read the inspected page’s resources, subject to per-URL access policy. - `UISourceCode`
- a DevTools front-end object representing a source file (a script, a stylesheet, or a source-map-derived original source such as
.ts/.scss) surfaced in the workspace. - Source map
- a mapping file (referenced by a
sourceMapURL) that reconstructs original source files; the reconstructed sources becomeUISourceCodes whose content really comes from the source-map URL, which may differ from the resource’s own URL (e.g. afile://or blocked URL). - `extensionAllowedOnContentProvider` / `extensionAllowedOnURL`
ExtensionServergates that decide whether a given extension may see/read a content provider, the latter checking a specific URL against the extension’s allowed hosts and file-access setting.
Root Cause Analysis
ExtensionServer.extensionAllowedOnContentProvider authorized a UISourceCode based on its own URL and its script sourceURL, but never considered the URL of the source map the UISourceCode’s content originated from. For source-map-derived sources (compiled JS/.ts via CompilerScriptMapping, or SASS/.scss via SASSSourceMapping), the real content is fetched from the source-map URL, so a source whose own URL is allowed could carry content pulled from a source map at a URL the extension is not permitted on — for example a file:///tmp/secret.map when allowFileAccess is false, or an otherwise blocked URL. This violated the invariant that an extension may only read content it is authorized for, letting a DevTools extension exfiltrate sourcesContent/original sources from disallowed source-map origins.
The fix adds sourceMapURLsForUISourceCode accessors down the binding stack (DebuggerWorkspaceBinding → CompilerScriptMapping, and CSSWorkspaceBinding → SASSSourceMapping) and makes extensionAllowedOnContentProvider collect all source-map URLs backing the UISourceCode and return false if extensionAllowedOnURL rejects any of them, so disallowed-source-map sources are hidden from getResources and reads.
UISourceCode only by its own URL while ignoring the source-map URL its content actually comes from; the fix enumerates those source-map URLs and denies access unless the extension is allowed on every one of them.Attack Path
- Install/run a DevTools extension
An attacker-controlled DevTools extension operates against an inspected target but lacks permission for a sensitive origin (e.g.
file://, or a blocked URL) —allowFileAccessis false. - A source map references a disallowed URL
The inspected page (or the attacker) causes a script/stylesheet to attach a source map hosted at a disallowed URL, e.g.
file:///tmp/secret.map, whose reconstructed original source becomes aUISourceCode. - Enumerate resources
The extension calls
chrome.devtools.inspectedWindow.getResources; pre-patch,extensionAllowedOnContentProviderapproved the derived source because only its own URL was checked, so the resource was exposed. - Read disallowed content
The extension reads the resource’s content (
getContent), obtainingsourcesContent/original source that originated from the disallowed source-map URL it was never authorized to access (CWE “Missing authorization”).
Impact Assessment
file:// source maps when file access is disallowed, or otherwise blocked URLs — an information-disclosure/authorization bypass within the DevTools front-end. The precondition is a DevTools extension running against a target where such a source map is present; the fix confirms allowFileAccess: true legitimately still exposes file://-derived sources. The advisory reports this as missing authorization in Extensions and does not state exploitation in the wild.Changed Functions
| Function | Change | Notes |
|---|---|---|
iffront_end/panels/common/ExtensionServer.test.ts |
modified | |
forfront_end/panels/common/ExtensionServer.test.ts |
modified | |
iffront_end/panels/common/ExtensionServer.ts |
modified |
Files Changed
front_end/models/bindings/SASSSourceMapping.tsfront_end/panels/common/ExtensionServer.test.tsfront_end/testing/ExtensionHelpers.tsfront_end/models/bindings/CSSWorkspaceBinding.test.tsfront_end/models/bindings/CompilerScriptMapping.tsfront_end/models/bindings/CSSWorkspaceBinding.tsfront_end/panels/common/ExtensionServer.tsfront_end/models/bindings/DebuggerWorkspaceBinding.ts
Audit Directions
- URL-of-record vs. URL-of-contentAudit access checks that authorize an object by its nominal URL while its actual bytes are fetched from a different URL (source maps, redirects, imported/eval sources, worklets).
- Derived resources in extension surfacesReview other
chrome.devtools/ExtensionServercontent-provider gates to ensure every URL that contributes content to a resource (not just the primary one) is passed throughextensionAllowedOnURL. - `file://` and blocked-URL leakage via indirectionLook for paths where
allowFileAccess/host restrictions are enforced on the top-level resource but bypassed when content is reached indirectly through mappings, symbols, or attached metadata.
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); + } + const debuggerWorkspaceBinding = backend.universe.debuggerWorkspaceBinding; + sinon.stub(Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding, 'instance').returns(debuggerWorkspaceBinding); + + const scriptInfo = {url: scriptUrl, content: scriptContent}; + const uiSourceCodePromise = debuggerWorkspaceBinding.waitForUISourceCodeAdded(scriptInfo.url, target); + const currentScript = await backend.addScript(target, scriptInfo, null); + await uiSourceCodePromise; + + const resources = await new Promise<Chrome.DevTools.Resource[]>(r => { + context.chrome.devtools?.inspectedWindow.getResources(r); + }); + const scriptResource = resources.find(item => item.url === scriptInfo.url.toString()); + if (!scriptResource) { + throw new Error(`Expected script resource to be registered for ${scriptInfo.url}`); + } + + return {target, currentScript, scriptResource}; +} + describe('Language Extension API', () => { const context = setupDevtoolsExtensionHooks(); it('reports loaded resources', async () => { @@ -1679,8 +1741,45 @@ }; assert.deepEqual(resource, expectedResource); }); + }); +for (const allowFileAccess of [false, true]) { + describe(`Source map resources with {allowFileAccess: ${allowFileAccess}}`, () => { + const context = setupDevtoolsExtensionHooks({allowFileAccess}); + + it(`${allowFileAccess ? 'exposes' : 'hides'} sources originating from a file:// source map`, async () => { + const backend = getBackend(context); + mockResourceTree(backend.cdpConnection); + const target = backend.createTarget({type: SDK.Target.Type.FRAME}); + target.setInspectedURL(urlString`http://example.com`); + sinon.stub(Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding, 'instance') + .returns(backend.universe.debuggerWorkspaceBinding); + + const sourceURL = urlString`http://example.com/source.ts`; + await backend.addScript(target, {url: urlString`http://example.com/eval.js`, content: '42;'}, { + url: 'file:///tmp/secret.map', + content: { + version: 3, + sources: [sourceURL], + sourcesContent: ['secret content'], + mappings: 'AAAA', + }, + }); + + const resources = await context.chrome.devtools?.inspectedWindow.getResources(); + const sourceResource = resources?.find(resource => resource.url === sourceURL); + + if (allowFileAccess) { + assert.exists(sourceResource); + assert.deepEqual(await sourceResource.getContent(), {content: 'secret content', encoding: ''}); + } else { + assert.notExists(sourceResource); + } + }); + }); +} + for (const allowFileAccess of [true, false]) { describe( `Language Extension API with {allowFileAccess: ${allowFileAccess}}`, () => { @@ -1730,29 +1829,13 @@ }); } -describe('validate attachSourceMapURL ', () => { +describe('attachSourceMapURL', () => { const context = setupDevtoolsExtensionHooks(); - let backend: MockDebuggerBackend; - let target: SDK.Target.Target; - let debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding; - - beforeEach(() => { - backend = getBackend(context); - mockResourceTree(backend.cdpConnection); - - target = backend.createTarget({type: SDK.Target.Type.FRAME}); - debuggerWorkspaceBinding = backend.universe.debuggerWorkspaceBinding; - sinon.stub(Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding, 'instance') - .returns(debuggerWorkspaceBinding); - }); - it('correctly attaches a source map to a registered script', async () => { const sourceRoot = 'http://example.com'; const scriptName = 'script.ts'; - const scriptInfo = { - url: urlString`${sourceRoot}/script.js`, - content: 'function f(x) { console.log(x); } function ignore(y){ console.log(y); }', - }; + const scriptUrl = urlString`${sourceRoot}/script.js`; + const scriptContent = 'function f(x) { console.log(x); } function ignore(y){ console.log(y); }'; const sourceMap = encodeSourceMap( [ `0:9 => ${scriptName}:0:1`, @@ -1765,38 +1848,20 @@ const sourceMapString = { version: 3, names: ['f', 'console', 'log', 'ignore'], - sources: [scriptInfo.url], + sources: [scriptUrl], mappings: sourceMap.mappings, - file: `${scriptInfo.url}.map`, + file: `${scriptUrl}.map`, }; - // Before any script is registered, there shouldn't be any uiSourceCodes. - assert.isNull(Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(scriptInfo.url)); - - // Create promise to await the uiSourceCode given the url and its target. - const uiSourceCodePromise = debuggerWorkspaceBinding.waitForUISourceCodeAdded(scriptInfo.url, target); - - // Register the script. - const currentScript = await backend.addScript(target, scriptInfo, null); - - // Await the promise for sourceCode to be added. - await uiSourceCodePromise; - - assert.exists(context.chrome.devtools); - - const resources = await context.chrome.devtools!.inspectedWindow.getResources(); - - // Validate that resource is registered. - assert.isTrue(resources && resources.length > 0); + const {currentScript, scriptResource} = await createScriptResource(context, {scriptUrl, scriptContent}); // Script should not have a source map url attached yet. assert.notExists(currentScript.sourceMapURL); // Call attachSourceMapURL with encoded source map as a dataURL - const scriptResource = resources.find(item => item.url === scriptInfo.url.toString()); const encodedSourceMap = `data:text/plain;base64,${btoa(JSON.stringify(sourceMapString))}`; - await scriptResource?.attachSourceMapURL(encodedSourceMap); + await scriptResource.attachSourceMapURL(encodedSourceMap); // Validate that the script has the sourcemap dataURL attached. assert.deepEqual(currentScript.sourceMapURL, encodedSourceMap); diff --git a/front_end/panels/common/ExtensionServer.ts b/front_end/panels/common/ExtensionServer.ts index 069f2f4..3ec8673 100644 --- a/front_end/panels/common/ExtensionServer.ts +++ b/front_end/panels/common/ExtensionServer.ts @@ -963,6 +963,18 @@ */ private extensionAllowedOnContentProvider( contentProvider: TextUtils.ContentProvider.ContentProvider, port: MessagePort): boolean { + if (contentProvider instanceof Workspace.UISourceCode.UISourceCode) { + const debuggerSourceMapURLs = + Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().sourceMapURLsForUISourceCode( + contentProvider); + const cssSourceMapURLs = + Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().sourceMapURLsForUISourceCode(contentProvider); + const sourceMapURLs = [...debuggerSourceMapURLs, ...cssSourceMapURLs]; + if (sourceMapURLs.some(url => !this.extensionAllowedOnURL(url, port))) { + return false; + } + } + // 1. Exception for Scripts with sourceURL if (contentProvider instanceof Workspace.UISourceCode.UISourceCode && contentProvider.contentType() === Common.ResourceType.resourceTypes.Script) { 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);
Regression Test / PoC
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);
Original Bug Report
Unvalidated sourceMapURL in AttachSourceMapToResource allows file:// access via DevTool extensions
Steps to reproduce the problem
- Create a devtools extension, which…
- Finds a script resource for a site, and then calls
- script.attachSourceMapURL(‘file:///C:/Windows/win.ini’)
Problem Description
In ExtensionServer.ts’ onAttachSourceMapToResource, the message.contentUrl is validated, but message.sourceMapURL is not. We should check for URLs that are blocked, and if so, fail.
Additional Comments
I have added new tests to ExtensionServer.test.ts (attached), which demonstrate the gap. I have a fix, which I will upload and link.
Summary
Unvalidated sourceMapURL in AttachSourceMapToResource allows file:// access via DevTool extensions
Custom Questions
Reporter credit:
Microsoft Edge
Additional Data
Category: Security
Chrome Channel: Stable
Regression: No \