CVE-2026-13968
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
iffront_end/models/trace/handlers/FramesHandler.ts |
modified | |
whilefront_end/models/trace/handlers/FramesHandler.ts |
modified |
Files Changed
front_end/models/trace/handlers/FramesHandler.ts
Patch
From a7f9ce9b3f6d3a79b17de55bd922531848ce38ae Mon Sep 17 00:00:00 2001
From: Alina Varkki <alinavarkki@google.com>
Date: Wed, 20 May 2026 12:17:31 +0200
Subject: [PATCH] Fix prototype pollution in trace FramesHandler using Map
Migrate mapFrames to a built-in Map class to prevent attacker-controlled trace frame keys from interacting with or mutating Object.prototype.
Bug: 513762145
Change-Id: I35f1091349a956d2bcad99a2292c2b695302854f
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7864400
Auto-Submit: Alina Varkki <alinavarkki@chromium.org>
Reviewed-by: Alex Rudenko <alexrudenko@chromium.org>
Commit-Queue: Alex Rudenko <alexrudenko@chromium.org>
---
diff --git a/front_end/models/trace/handlers/FramesHandler.ts b/front_end/models/trace/handlers/FramesHandler.ts
index d148349..181b4ed 100644
--- a/front_end/models/trace/handlers/FramesHandler.ts
+++ b/front_end/models/trace/handlers/FramesHandler.ts
@@ -507,26 +507,28 @@
private queueFrames: number[] = [];
// Maps frameSeqId to BeginFrameInfo.
- private mapFrames: Record<number, BeginFrameInfo> = {};
+ private mapFrames = new Map<number, BeginFrameInfo>();
// Add a BeginFrame to the queue, if it does not already exit.
addFrameIfNotExists(seqId: number, startTime: Types.Timing.Micro, isDropped: boolean, isPartial: boolean): void {
- if (!(seqId in this.mapFrames)) {
- this.mapFrames[seqId] = new BeginFrameInfo(seqId, startTime, isDropped, isPartial);
+ if (!this.mapFrames.has(seqId)) {
+ this.mapFrames.set(seqId, new BeginFrameInfo(seqId, startTime, isDropped, isPartial));
this.queueFrames.push(seqId);
}
}
// Set a BeginFrame in queue as dropped.
setDropped(seqId: number, isDropped: boolean): void {
- if (seqId in this.mapFrames) {
- this.mapFrames[seqId].isDropped = isDropped;
+ const frame = this.mapFrames.get(seqId);
+ if (frame) {
+ frame.isDropped = isDropped;
}
}
setPartial(seqId: number, isPartial: boolean): void {
- if (seqId in this.mapFrames) {
- this.mapFrames[seqId].isPartial = isPartial;
+ const frame = this.mapFrames.get(seqId);
+ if (frame) {
+ frame.isPartial = isPartial;
}
}
@@ -535,7 +537,7 @@
// Do not visualize this frame in the rare case where the current DrawFrame
// does not have a corresponding BeginFrame.
- if (seqId in this.mapFrames) {
+ if (this.mapFrames.has(seqId)) {
// Pop all BeginFrames before the current frame, and add only the dropped
// ones in |frames_to_visualize|.
// Non-dropped frames popped here are BeginFrames that are never
@@ -544,17 +546,21 @@
// be naturally presented as continuationss of other frames.
while (this.queueFrames[0] !== seqId) {
const currentSeqId = this.queueFrames[0];
- if (this.mapFrames[currentSeqId].isDropped) {
- framesToVisualize.push(this.mapFrames[currentSeqId]);
+ const currentFrame = this.mapFrames.get(currentSeqId);
+ if (currentFrame && currentFrame.isDropped) {
+ framesToVisualize.push(currentFrame);
}
- delete this.mapFrames[currentSeqId];
+ this.mapFrames.delete(currentSeqId);
this.queueFrames.shift();
}
// Pop the BeginFrame associated with the current DrawFrame.
- framesToVisualize.push(this.mapFrames[seqId]);
- delete this.mapFrames[seqId];
+ const frame = this.mapFrames.get(seqId);
+ if (frame) {
+ framesToVisualize.push(frame);
+ }
+ this.mapFrames.delete(seqId);
this.queueFrames.shift();
}
return framesToVisualize;
Original Bug Report
Prototype Pollution in DevTools Performance Panel via Malicious Trace File
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The DevTools Performance panel trace parser is vulnerable to potential prototype pollution when processing malicious trace files. An attacker-controlled ‘frameSeqId’ key can be used to mutate Object.prototype in the privileged devtools:// renderer process.
Affected files:
third_party/devtools-frontend/src/front_end/models/trace/handlers/FramesHandler.tsthird_party/devtools-frontend/src/front_end/models/trace/types/TraceEvents.tsthird_party/devtools-frontend/src/front_end/panels/timeline/TimelinePanel.ts
Estimated timestamp from git blame: 2021-09-06
Summary
A potential prototype pollution vulnerability exists in the Chromium DevTools Performance panel’s trace engine. When a user imports a specially crafted trace file, the parser uses attacker-controlled keys to perform property assignments on a plain JavaScript object. This can lead to the mutation of Object.prototype within the privileged devtools:// renderer process.
Root Cause Analysis
The vulnerability resides in the TimelineFrameBeginFrameQueue class in third_party/devtools-frontend/src/front_end/models/trace/handlers/FramesHandler.ts. This class uses a plain JavaScript object ({}) as a map to track frame information:
export class TimelineFrameBeginFrameQueue {
private mapFrames: Record<number, BeginFrameInfo> = {}; // Plain object
setDropped(seqId: number, isDropped: boolean): void {
if (seqId in this.mapFrames) {
this.mapFrames[seqId].isDropped = isDropped;
}
}
}
The seqId parameter is derived directly from the frameSeqId field of DroppedFrame events in the trace JSON. If an attacker provides the string "__proto__" as the frameSeqId, the following occurs:
- The check
seqId in this.mapFrames(effectively"__proto__" in {}) evaluates totruebecause__proto__is inherited fromObject.prototype. - The assignment
this.mapFrames[seqId].isDropped = isDroppedresolves toObject.prototype.isDropped = isDropped.
This pollutes the global Object.prototype for the entire renderer process. Similar patterns exist in setPartial and in other handlers like ScriptsHandler.ts and SamplesHandler.ts where trace-derived keys are used with plain objects.
Potential Attack Vector
An attacker could distribute a malicious Performance trace file (JSON). If a user loads this file into DevTools, the prototype pollution is triggered. Given that the DevTools frontend is a complex JavaScript application running in a privileged origin (devtools://), polluting Object.prototype could potentially lead to secondary vulnerabilities such as cross-site scripting (XSS) or remote code execution (RCE) within the renderer process by overriding configuration or template properties used by the UI.
Suggested Steps to Reproduce
Note: These steps are potential/suggested as they have not been executed by our automated tools.
- Create a trace file
pollute.jsonwith the following content:[ {"name": "TracingStartedInBrowser", "ph": "I", "args": {"data": {"frames": [{"frame": "1"}]}}}, {"name": "DroppedFrame", "ph": "I", "cat": "disabled-by-default-devtools.timeline.frame", "ts": 1000, "args": {"layerTreeId": 1, "frameSeqId": "__proto__", "hasPartialUpdate": true}} ] - Open Chrome DevTools and go to the Performance panel.
- Click the Load profile button and select
pollute.json. - Once loaded, check the console for pollution:
console.log(({}).isDropped);. If the value istrue, the prototype has been polluted.
Recommended Fix
The mapFrames object (and similar map objects in trace handlers) should be initialized with a null prototype to prevent access to Object.prototype properties:
private mapFrames: Record<number, BeginFrameInfo> = Object.create(null);
Alternatively, use the built-in Map class, which is designed for key-value storage and is not susceptible to prototype pollution.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.