CVE-2026-7351
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/download/mhtml_generation_manager.cc |
modified |
Files Changed
content/browser/download/mhtml_generation_manager.cc
Patch
From 685289fef67dbd93e761bb720d8cd1688d391748 Mon Sep 17 00:00:00 2001
From: Lukasz Anforowicz <lukasza@chromium.org>
Date: Wed, 15 Apr 2026 13:34:42 -0700
Subject: [PATCH] [mhtml] Store pending documents as `base::WeakPtr<RenderFrameHostImpl>`.
This is more correct, as the document stored in a `FrameTreeNode` can
change over the lifetime of the node, but with "Render Document" changes
`RenderFrameHostImpl` is associated with a specific document.
Fixed: 499119490
Change-Id: Ic6231c4f8d7bca362a057369d1ca95e794b9b012
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7762708
Commit-Queue: Łukasz Anforowicz <lukasza@chromium.org>
Reviewed-by: Nasko Oskov <nasko@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1615376}
---
diff --git a/content/browser/download/mhtml_generation_manager.cc b/content/browser/download/mhtml_generation_manager.cc
index 61cc2eb..511dd0f4 100644
--- a/content/browser/download/mhtml_generation_manager.cc
+++ b/content/browser/download/mhtml_generation_manager.cc
@@ -12,6 +12,7 @@
#include "base/files/file.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
+#include "base/memory/weak_ptr.h"
#include "base/numerics/safe_conversions.h"
#include "base/stl_util.h"
#include "base/strings/string_util.h"
@@ -252,8 +253,8 @@
// choices.
MHTMLGenerationParams params_;
- // The IDs of frames that still need to be processed.
- base::queue<FrameTreeNodeId> pending_frame_tree_node_ids_;
+ // The documents that still need to be processed.
+ base::queue<base::WeakPtr<RenderFrameHostImpl>> pending_render_frame_hosts_;
// Identifies a frame to which we've sent through
// MhtmlFileWriter::SerializeAsMHTML but for which we didn't yet process
@@ -332,13 +333,12 @@
// Skip inner tree placeholder nodes.
continue;
}
- pending_frame_tree_node_ids_.push(node->frame_tree_node_id());
+ pending_render_frame_hosts_.push(node->current_frame_host()->GetWeakPtr());
}
// Main frame needs to be processed first.
- DCHECK(!pending_frame_tree_node_ids_.empty());
- DCHECK(FrameTreeNode::GloballyFindByID(pending_frame_tree_node_ids_.front())
- ->parent() == nullptr);
+ CHECK(!pending_render_frame_hosts_.empty());
+ CHECK(pending_render_frame_hosts_.front().get()->is_main_frame());
// Save off any extra data.
auto* extra_parts = static_cast<MHTMLExtraPartsImpl*>(
@@ -369,16 +369,14 @@
}
mojom::MhtmlSaveStatus MHTMLGenerationManager::Job::SendToNextRenderFrame() {
- DCHECK(browser_file_.IsValid());
- DCHECK(!pending_frame_tree_node_ids_.empty());
+ CHECK(browser_file_.IsValid());
+ CHECK(!pending_render_frame_hosts_.empty());
- FrameTreeNodeId frame_tree_node_id = pending_frame_tree_node_ids_.front();
- pending_frame_tree_node_ids_.pop();
-
- FrameTreeNode* ftn = FrameTreeNode::GloballyFindByID(frame_tree_node_id);
- if (!ftn) // The contents went away.
+ RenderFrameHostImpl* rfh = pending_render_frame_hosts_.front().get();
+ pending_render_frame_hosts_.pop();
+ if (!rfh) { // The contents went away.
return mojom::MhtmlSaveStatus::kFrameNoLongerExists;
- RenderFrameHost* rfh = ftn->current_frame_host();
+ }
if (writer_) {
// If we reached here, means the work for previous frame is done, so it is
@@ -403,7 +401,8 @@
// Send a Mojo request to Renderer to serialize its frame.
DCHECK(frame_tree_node_id_of_busy_frame_.is_null());
- frame_tree_node_id_of_busy_frame_ = frame_tree_node_id;
+ frame_tree_node_id_of_busy_frame_ =
+ rfh->frame_tree_node()->frame_tree_node_id();
auto response_callback = base::BindOnce(&Job::SerializeAsMHTMLResponse,
weak_factory_.GetWeakPtr());
@@ -542,7 +541,7 @@
// If current operation is successful and there are more frames to process,
// let save status depend on the result of sending the next request.
if (save_status == mojom::MhtmlSaveStatus::kSuccess &&
- !pending_frame_tree_node_ids_.empty() && CurrentFrameDone()) {
+ !pending_render_frame_hosts_.empty() && CurrentFrameDone()) {
save_status = SendToNextRenderFrame();
}
@@ -555,8 +554,9 @@
// Otherwise report completion if there are no more frames to process
// and Job is done processing the current frame.
- if (pending_frame_tree_node_ids_.empty() && CurrentFrameDone())
+ if (pending_render_frame_hosts_.empty() && CurrentFrameDone()) {
Finalize(mojom::MhtmlSaveStatus::kSuccess);
+ }
}
bool MHTMLGenerationManager::Job::CurrentFrameDone() const {
Original Bug Report
TOCTOU in MHTMLGenerationManager allows DOM exfiltration via BFCache race
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 without the security team.
Overview: A Time-of-Check to Time-of-Use (TOCTOU) vulnerability exists between the chrome.pageCapture.saveAsMHTML API and MHTMLGenerationManager. An extension can race an asynchronous file creation task by triggering a fast BFCache restoration navigation, tricking the browser into serializing the DOM of a restricted page instead of the authorized one.
Affected files:
content/browser/download/mhtml_generation_manager.ccchrome/browser/extensions/api/page_capture/page_capture_api.cc
Estimated timestamp from git blame: 2024-09-04
Summary
A Time-of-Check to Time-of-Use (TOCTOU) vulnerability exists in the MHTMLGenerationManager when combined with the chrome.pageCapture.saveAsMHTML extension API.
The saveAsMHTML API performs a permission check and verifies the target document’s ID before initiating MHTML generation. However, MHTMLGenerationManager::Job::initializeJob captures only the persistent FrameTreeNodeIds of the page before posting an asynchronous task to a background thread to create the target file (CreateMHTMLFile).
While the background task is executing, an attacker can trigger a fast navigation, such as restoring a restricted page (e.g., chrome://settings) from the Back/Forward Cache (BFCache). When the file creation task finishes and execution resumes on the UI thread, MHTMLGenerationManager::Job::SendToNextRenderFrame fetches the current_frame_host() for each captured FrameTreeNodeId. Because the BFCache navigation has already committed, the job will instruct the restricted page’s RenderFrameHost to serialize its DOM, bypassing the extension API’s origin checks and exfiltrating sensitive data.
Technical Details
PageCaptureSaveAsMHTMLFunction::Run()checks permissions and saves theDocumentId(document_id_) of the authorized page.- After creating a temporary file,
TemporaryFileCreatedOnUIverifies that the current main frame’sDocumentIdstill matchesdocument_id_. If so, it callsWebContents::GenerateMHTML(). MHTMLGenerationManager::Job::initializeJob()takes a snapshot of the frame tree by storingFrameTreeNodeIds intopending_frame_tree_node_ids_.initializeJob()posts an asynchronous task toGetDownloadTaskRunner()to callCreateMHTMLFile.- During this task hop, a fast navigation (e.g., a BFCache restore) can commit on the UI thread, swapping the document in the main frame’s
FrameTreeNode. - The background task completes, and
MHTMLGenerationManager::Job::OnFileAvailableresumes on the UI thread. SendToNextRenderFrame()is called. It pops aFrameTreeNodeIdand callsftn->current_frame_host(). This now returns the newly restoredRenderFrameHost(the restricted page).- The browser calls
writer_->SerializeAsMHTML()on the restrictedRenderFrameHost. The renderer process does not perform URL or permission checks and serializes the restricted DOM.
Potential Steps to Reproduce
Note: These steps are suggested based on code analysis; a working PoC has not yet been executed.
- A malicious extension (with
pageCapturepermission) opens a tab to a restricted URL likechrome://settings. - The extension navigates the tab away to an attacker-controlled origin (
https://attacker.com), placingchrome://settingsinto the BFCache. - The extension calls
chrome.pageCapture.saveAsMHTML({tabId: ...}). - Immediately after, the extension triggers a fast navigation back to the restricted page:
chrome.tabs.goBack(...). - The BFCache restoration commits quickly, racing the background file creation task (
CreateMHTMLFile). - The MHTML generation process completes using the newly restored document.
- The extension reads the resulting MHTML file to access the DOM of
chrome://settings.
Suggested Fix
Instead of storing persistent FrameTreeNodeIds, MHTMLGenerationManager::Job should store GlobalRenderFrameHostIds or the DocumentId for each frame during initializeJob. When iterating in SendToNextRenderFrame(), the job should verify that the RenderFrameHost still matches the saved ID before proceeding with serialization. Alternatively, the job should abort if a navigation occurs while it is pending.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.