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 <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.
References
On This Page