Medium watch CVSS 7.5 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Extensions
DescriptionMissing authorization in Extensions
ComponentExtensions
Sub-componentPlatform>DevTools>Extensions
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker523313374
Fix commitecaa2e8404a1 (devtools/devtools-frontend) +150/-44
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedMicrosoft Edge
Reported2026-06-12 (per vendor advisory)
Fix landed2026-08-10 (public git)
Disclosed2026-09-08
Patch gap29 days — fix public in source before the advisory

Background

DevTools extension
a third-party extension using the chrome.devtools API (here via ExtensionServer) 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 become UISourceCodes whose content really comes from the source-map URL, which may differ from the resource’s own URL (e.g. a file:// or blocked URL).
`extensionAllowedOnContentProvider` / `extensionAllowedOnURL`
ExtensionServer gates 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.

Key insight
The core mistake was authorizing a source-derived 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

  1. 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) — allowFileAccess is false.
  2. 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 a UISourceCode.
  3. Enumerate resources The extension calls chrome.devtools.inspectedWindow.getResources; pre-patch, extensionAllowedOnContentProvider approved the derived source because only its own URL was checked, so the resource was exposed.
  4. Read disallowed content The extension reads the resource’s content (getContent), obtaining sourcesContent/original source that originated from the disallowed source-map URL it was never authorized to access (CWE “Missing authorization”).

Impact Assessment

A DevTools extension could read source content originating from source-map URLs it is not permitted on — notably 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

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

Files Changed

  • front_end/models/bindings/SASSSourceMapping.ts
  • front_end/panels/common/ExtensionServer.test.ts
  • front_end/testing/ExtensionHelpers.ts
  • front_end/models/bindings/CSSWorkspaceBinding.test.ts
  • front_end/models/bindings/CompilerScriptMapping.ts
  • front_end/models/bindings/CSSWorkspaceBinding.ts
  • front_end/panels/common/ExtensionServer.ts
  • front_end/models/bindings/DebuggerWorkspaceBinding.ts

Audit Directions

  • URL-of-record vs. URL-of-content
    Audit 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 surfaces
    Review other chrome.devtools/ExtensionServer content-provider gates to ensure every URL that contributes content to a resource (not just the primary one) is passed through extensionAllowedOnURL.
  • `file://` and blocked-URL leakage via indirection
    Look 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.
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);
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

reported by [email protected]

Unvalidated sourceMapURL in AttachSourceMapToResource allows file:// access via DevTool extensions

Steps to reproduce the problem

  1. Create a devtools extension, which…
  2. Finds a script resource for a site, and then calls
  3. 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 \

View on issue tracker