Chrome · DevTools
CVE-2026-113132
Memory Corruption in DevTools
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
iffront_end/core/sdk/SourceMapCache.ts |
modified | |
iffront_end/core/sdk/SourceMapManager.ts |
modified |
Files Changed
front_end/core/sdk/SourceMapCache.test.tsfront_end/core/sdk/SourceMapCache.tsfront_end/core/sdk/SecurityOrigin.tsfront_end/core/sdk/SourceMapManager.test.tsfront_end/core/sdk/SecurityOrigin.test.tsfront_end/core/sdk/SourceMapManager.ts
Patch
From 0110657e1fbce5c59bc8c499e826f747399d5a71 Mon Sep 17 00:00:00 2001 From: Danil Somsikov <[email protected]> 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 <[email protected]> Commit-Queue: Philip Pfaffe <[email protected]> Reviewed-by: Philip Pfaffe <[email protected]> --- 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.
References
On This Page