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 <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);
+ }
+ 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 \