Low chrome Memory Corruption 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in DevTools
DescriptionMemory corruption in DevTools
ComponentDevTools
Bug ClassMemory Corruption
Tracker562953023
Fix commit0110657e1fbc (devtools/devtools-frontend) +145/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
front_end/core/sdk/SourceMapCache.ts
modified
if
front_end/core/sdk/SourceMapManager.ts
modified

Files Changed

  • front_end/core/sdk/SourceMapCache.test.ts
  • front_end/core/sdk/SourceMapCache.ts
  • front_end/core/sdk/SecurityOrigin.ts
  • front_end/core/sdk/SourceMapManager.test.ts
  • front_end/core/sdk/SecurityOrigin.test.ts
  • front_end/core/sdk/SourceMapManager.ts
From 0110657e1fbce5c59bc8c499e826f747399d5a71 Mon Sep 17 00:00:00 2001
From: Danil Somsikov <dsv@chromium.org>
Date: Mon, 21 Sep 2026 10:39:45 -0700
Subject: [PATCH] Disallow source map caching for opaque origins and isolate file URLs

Previously, source map caching used Common.ParsedURL.extractOrigin(), which collapsed file:// URLs together and allowed empty or opaque origins to share cache entries. This could allow cache collisions or cache poisoning between different local files or anonymous scripts.

This change:
- Treats blob:file URLs as opaque origins in SecurityOrigin.
- Disallows caching source maps for empty, opaque, or collapsed file:// origins in SourceMapCache.
- Uses SecurityOrigin.siteId() in SourceMapManager to properly isolate file URLs and skip caching for anonymous or opaque initiator scripts.

Bug: 562953023
Change-Id: I07396838801b9753f47d456a022cf04edbe39b03
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/8433922
Auto-Submit: Danil Somsikov <dsv@chromium.org>
Commit-Queue: Philip Pfaffe <pfaffe@chromium.org>
Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
---
diff --git a/front_end/core/sdk/SecurityOrigin.test.ts b/front_end/core/sdk/SecurityOrigin.test.ts
index 342f4a9..623b218 100644
--- a/front_end/core/sdk/SecurityOrigin.test.ts
+++ b/front_end/core/sdk/SecurityOrigin.test.ts
@@ -40,6 +40,8 @@
     it('treats opaque URLs as unique opaque origins that do not match each other', () => {
       const data1 = SDK.SecurityOrigin.SecurityOrigin.create('data:text/html,hello');
       const data2 = SDK.SecurityOrigin.SecurityOrigin.create('data:text/html,hello');
+      const blobFile1 = SDK.SecurityOrigin.SecurityOrigin.create('blob:file:///1234-5678');
+      const blobFile2 = SDK.SecurityOrigin.SecurityOrigin.create('blob:file:///1234-5678');
       const aboutBlank = SDK.SecurityOrigin.SecurityOrigin.create('about:blank');
       const nullOrigin = SDK.SecurityOrigin.SecurityOrigin.create('null');
       const emptyOrigin = SDK.SecurityOrigin.SecurityOrigin.create('');
@@ -47,12 +49,15 @@
 
       assert.isTrue(data1.isOpaque());
       assert.isTrue(data2.isOpaque());
+      assert.isTrue(blobFile1.isOpaque());
+      assert.isTrue(blobFile2.isOpaque());
       assert.isTrue(aboutBlank.isOpaque());
       assert.isTrue(nullOrigin.isOpaque());
       assert.isTrue(emptyOrigin.isOpaque());
       assert.isTrue(detachedOrigin.isOpaque());
 
       assert.isFalse(data1.isSameOriginWith(data2));
+      assert.isFalse(blobFile1.isSameOriginWith(blobFile2));
       assert.isFalse(data1.isSameOriginWith(aboutBlank));
       assert.isFalse(nullOrigin.isSameOriginWith(emptyOrigin));
       assert.isFalse(detachedOrigin.isSameOriginWith(emptyOrigin));
diff --git a/front_end/core/sdk/SecurityOrigin.ts b/front_end/core/sdk/SecurityOrigin.ts
index 6350a31..92011e7 100644
--- a/front_end/core/sdk/SecurityOrigin.ts
+++ b/front_end/core/sdk/SecurityOrigin.ts
@@ -28,6 +28,7 @@
   'about:',
   'blob:about',
   'blob:data',
+  'blob:file',
   'blob:null',
 ] as const;
 
diff --git a/front_end/core/sdk/SourceMapCache.test.ts b/front_end/core/sdk/SourceMapCache.test.ts
index 8a138b2..b7ab248 100644
--- a/front_end/core/sdk/SourceMapCache.test.ts
+++ b/front_end/core/sdk/SourceMapCache.test.ts
@@ -90,7 +90,7 @@
     assert.isNull(await cache.get(debugId, origin2));
   });
 
-  it('supports empty security origin', async () => {
+  it('rejects empty and collapsed file:// security origins', async () => {
     const map: SDK.SourceMap.SourceMapV3 = {
       version: 3,
       sources: ['foo.ts'],
@@ -98,7 +98,9 @@
     };
     const debugId = '1' as SDK.SourceMap.DebugId;
     await cache.set(debugId, Platform.DevToolsPath.EmptyUrlString, map);
+    await cache.set(debugId, urlString`file://`, map);
 
-    assert.deepEqual(await cache.get(debugId, Platform.DevToolsPath.EmptyUrlString), map);
+    assert.isNull(await cache.get(debugId, Platform.DevToolsPath.EmptyUrlString));
+    assert.isNull(await cache.get(debugId, urlString`file://`));
   });
 });
diff --git a/front_end/core/sdk/SourceMapCache.ts b/front_end/core/sdk/SourceMapCache.ts
index 7ab390b..305fc68 100644
--- a/front_end/core/sdk/SourceMapCache.ts
+++ b/front_end/core/sdk/SourceMapCache.ts
@@ -28,11 +28,17 @@
   }
 
   async set(debugId: DebugId, securityOrigin: Platform.DevToolsPath.UrlString, sourceMap: SourceMapV3): Promise<void> {
+    if (!securityOrigin || securityOrigin === 'file://') {
+      return;
+    }
     const cache = await this.#cache();
     await cache?.put(SourceMapCache.#urlForDebugId(debugId, securityOrigin), new Response(JSON.stringify(sourceMap)));
   }
 
   async get(debugId: DebugId, securityOrigin: Platform.DevToolsPath.UrlString): Promise<SourceMapV3|null> {
+    if (!securityOrigin || securityOrigin === 'file://') {
+      return null;
+    }
     const cache = await this.#cache();
     const response = await cache?.match(SourceMapCache.#urlForDebugId(debugId, securityOrigin));
     return (await response?.json() as SourceMapV3 | undefined) ?? null;
@@ -65,10 +71,16 @@
   readonly #cache = new Map<string, SourceMapV3>();
 
   async set(debugId: DebugId, securityOrigin: Platform.DevToolsPath.UrlString, sourceMap: SourceMapV3): Promise<void> {
+    if (!securityOrigin || securityOrigin === 'file://') {
+      return;
+    }
     this.#cache.set(`${debugId}|${securityOrigin}`, sourceMap);
   }
 
   async get(debugId: DebugId, securityOrigin: Platform.DevToolsPath.UrlString): Promise<SourceMapV3|null> {
+    if (!securityOrigin || securityOrigin === 'file://') {
+      return null;
+    }
     return this.#cache.get(`${debugId}|${securityOrigin}`) ?? null;
   }
 
diff --git a/front_end/core/sdk/SourceMapManager.test.ts b/front_end/core/sdk/SourceMapManager.test.ts
index ca6c194..4a0e207 100644
--- a/front_end/core/sdk/SourceMapManager.test.ts
+++ b/front_end/core/sdk/SourceMapManager.test.ts
@@ -480,6 +480,112 @@
       const storedMap = await sourceMapCache.get(debugId, clientOrigin);
       assert.deepEqual(storedMap, networkMap);
     });
+
+    it('isolates cached source maps across different file:// URLs', async () => {
+      const debugId = 'test-debug-id' as SDK.SourceMap.DebugId;
+      const attackerFileUrl = urlString`file:///tmp/attacker/poison.js`;
+      const victimFileUrl = urlString`file:///tmp/victim/dist/app.js`;
+
+      const poisonedMap: SDK.SourceMap.SourceMapV3 = {
+        version: 3,
+        sources: ['forged.ts'],
+        mappings: '',
+        debugId,
+      };
+
+      const genuineMap: SDK.SourceMap.SourceMapV3 = {
+        version: 3,
+        sources: ['genuine.ts'],
+        mappings: '',
+        debugId,
+      };
+
+      const loadResourceSpy = sinon.spy();
+      const universe = new TestUniverse({
+        pageResourceLoaderOptions: {
+          loadOverride: async url => {
+            loadResourceSpy(url);
+            return {
+              success: true,
+              content: JSON.stringify(genuineMap),
+              errorDescription: {message: '', statusCode: 0, netError: 0, netErrorName: '', urlValid: true},
+            };
+          },
+        },
+      });
+      const target = universe.createTarget();
+      const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
+
+      await sourceMapCache.set(debugId, attackerFileUrl, poisonedMap);
+
+      const victimClient = new MockClient(target, debugId, victimFileUrl);
+      sourceMapManager.attachSourceMap(victimClient, sourceURL, sourceMappingURL,
+                                       SDK.SourceMap.SourceMapProvenance.CDP);
+      const victimSourceMap = await sourceMapManager.sourceMapForClientPromise(victimClient);
+
+      assert.isNotNull(victimSourceMap);
+      assert.deepEqual(victimSourceMap?.sourceURLs(), [urlString`http://localhost/genuine.ts`]);
+      assert.strictEqual(loadResourceSpy.callCount, 1,
+                         'loadResource should have been called for a different file:// path');
+
+      const storedVictimMap = await sourceMapCache.get(debugId, victimFileUrl);
+      assert.deepEqual(storedVictimMap, genuineMap);
+
+      // Same file:// path should hit the cache without re-fetching.
+      const sameVictimClient = new MockClient(target, debugId, victimFileUrl);
+      sourceMapManager.attachSourceMap(sameVictimClient, sourceURL, sourceMappingURL,
+                                       SDK.SourceMap.SourceMapProvenance.CDP);
+      const cachedVictimSourceMap = await sourceMapManager.sourceMapForClientPromise(sameVictimClient);
+      assert.isNotNull(cachedVictimSourceMap);
+      assert.deepEqual(cachedVictimSourceMap?.sourceURLs(), [urlString`http://localhost/genuine.ts`]);
+      assert.strictEqual(loadResourceSpy.callCount, 1,
+                         'loadResource should not be called again for the same file:// path');
+    });
+
+    it('skips caching for scripts with no initiator URL or opaque initiator URLs', async () => {
+      const debugId = 'test-debug-id' as SDK.SourceMap.DebugId;
+      const map1: SDK.SourceMap.SourceMapV3 = {
+        version: 3,
+        sources: ['eval1.ts'],
+        mappings: '',
+        debugId,
+      };
+      const map2: SDK.SourceMap.SourceMapV3 = {
+        version: 3,
+        sources: ['eval2.ts'],
+        mappings: '',
+        debugId,
+      };
+
+      let callCount = 0;
+      const universe = new TestUniverse({
+        pageResourceLoaderOptions: {
+          loadOverride: async () => {
+            callCount++;
+            return {
+              success: true,
+              content: JSON.stringify(callCount === 1 ? map1 : map2),
+              errorDescription: {message: '', statusCode: 0, netError: 0, netErrorName: '', urlValid: true},
+            };
+          },
+        },
+      });
+      const target = universe.createTarget();
+      const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
+
+      const anonymousClient1 = new MockClient(target, debugId, null);
+      sourceMapManager.attachSourceMap(anonymousClient1, sourceURL, sourceMappingURL,
+                                       SDK.SourceMap.SourceMapProvenance.CDP);
+      const sourceMap1 = await sourceMapManager.sourceMapForClientPromise(anonymousClient1);
+      assert.deepEqual(sourceMap1?.sourceURLs(), [urlString`http://localhost/eval1.ts`]);
+
+      const anonymousClient2 = new MockClient(target, debugId, Platform.DevToolsPath.EmptyUrlString);
+      sourceMapManager.attachSourceMap(anonymousClient2, sourceURL, sourceMappingURL,
+                                       SDK.SourceMap.SourceMapProvenance.CDP);
+      const sourceMap2 = await sourceMapManager.sourceMapForClientPromise(anonymousClient2);
+      assert.deepEqual(sourceMap2?.sourceURLs(), [urlString`http://localhost/eval2.ts`]);
+      assert.strictEqual(callCount, 2, 'anonymous scripts must not share a cache entry');
+    });
   });
 
   describe('lazy loading source maps', () => {
diff --git a/front_end/core/sdk/SourceMapManager.ts b/front_end/core/sdk/SourceMapManager.ts
index 1ffe8fe..593c883 100644
--- a/front_end/core/sdk/SourceMapManager.ts
+++ b/front_end/core/sdk/SourceMapManager.ts
@@ -7,6 +7,7 @@
 
 import type {FrameAssociated} from './FrameAssociated.js';
 import {PageResourceLoader, type PageResourceLoadInitiator, type ResourceLoader} from './PageResourceLoader.js';
+import {SecurityOrigin} from './SecurityOrigin.js';
 import {type DebugId, parseSourceMap, SourceMap, type SourceMapProvenance, type SourceMapV3} from './SourceMap.js';
 import {SourceMapCache} from './SourceMapCache.js';
 import {type Target, Type} from './Target.js';
@@ -215,14 +216,24 @@
   }
 }
 
+function getCacheOrigin(initiator: PageResourceLoadInitiator): Platform.DevToolsPath.UrlString|null {
+  if (!initiator.initiatorUrl) {
+    return null;
+  }
+  const securityOrigin = SecurityOrigin.create(initiator.initiatorUrl);
+  if (securityOrigin.isOpaque() || (securityOrigin.isFile() && securityOrigin.siteId() === 'file:///')) {
+    return null;
+  }
+  return securityOrigin.siteId() as Platform.DevToolsPath.UrlString;
+}
+
 async function loadSourceMap(resourceLoader: ResourceLoader, sourceMapCache: SourceMapCache,
                              url: Platform.DevToolsPath.UrlString, debugId: DebugId|null,
                              initiator: PageResourceLoadInitiator): Promise<SourceMapV3> {
   try {
-    if (debugId) {
-      const securityOrigin = initiator.initiatorUrl ? Common.ParsedURL.ParsedURL.extractOrigin(initiator.initiatorUrl) :
-                                                      Platform.DevToolsPath.EmptyUrlString;
-      const cachedSourceMap = await sourceMapCache.get(debugId, securityOrigin);
+    const cacheOrigin = debugId ? getCacheOrigin(initiator) : null;
+    if (debugId && cacheOrigin) {
+      const cachedSourceMap = await sourceMapCache.get(debugId, cacheOrigin);
       if (cachedSourceMap) {
         return cachedSourceMap;
       }
@@ -230,11 +241,9 @@
 
     const {content} = await resourceLoader.loadResource(url, initiator);
     const sourceMap = parseSourceMap(content);
-    if (debugId && 'debugId' in sourceMap && sourceMap.debugId === debugId) {
+    if (debugId && cacheOrigin && 'debugId' in sourceMap && sourceMap.debugId === debugId) {
       // In case something goes wrong with updating the cache, we still want to use the source map.
-      const securityOrigin = initiator.initiatorUrl ? Common.ParsedURL.ParsedURL.extractOrigin(initiator.initiatorUrl) :
-                                                      Platform.DevToolsPath.EmptyUrlString;
-      await sourceMapCache.set(sourceMap.debugId as DebugId, securityOrigin, sourceMap).catch();
+      await sourceMapCache.set(sourceMap.debugId as DebugId, cacheOrigin, sourceMap).catch();
     }
     return sourceMap;
   } catch (cause) {
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.