CVE-2026-19152
Overview
Files Changed
chrome/browser/repost_form_warning_browsertest.cccontent/browser/renderer_host/navigation_request.cc
Patch
From 202324afa3f42ed4c5480eca5c0ea94a08935ec1 Mon Sep 17 00:00:00 2001
From: Charlie Reis <creis@chromium.org>
Date: Thu, 03 Sep 2026 14:00:37 -0700
Subject: [PATCH] Ensure Confirm Form Resubmission dialog works after an error page.
POST data should only be cleared in cases where it may incorrectly be
given to the renderer process of the current page or a blocked error
page, not on a failed navigation that may be retried.
This CL was partly written by Gemini.
Bug: 553614977, 531165110
Change-Id: Ic6f42f2db8e0ca36e231f56f7c7b49efe935a9bd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8341516
Reviewed-by: Avi Drissman <avi@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Commit-Queue: Avi Drissman <avi@chromium.org>
Auto-Submit: Charlie Reis <creis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1691913}
---
diff --git a/chrome/browser/repost_form_warning_browsertest.cc b/chrome/browser/repost_form_warning_browsertest.cc
index d7ce5b7..425e2300 100644
--- a/chrome/browser/repost_form_warning_browsertest.cc
+++ b/chrome/browser/repost_form_warning_browsertest.cc
@@ -11,14 +11,22 @@
#include "chrome/common/url_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
+#include "components/strings/grit/components_strings.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
+#include "net/base/net_errors.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "ui/base/l10n/l10n_util.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
+#include "ui/views/test/dialog_test.h"
+#include "ui/views/test/widget_test.h"
+#include "ui/views/widget/widget.h"
using web_modal::WebContentsModalDialogManager;
@@ -121,3 +129,98 @@
ShowAndVerifyUi();
}
#endif
+
+// Verifies that confirming form resubmission after navigating back to an
+// uncacheable POST page resubmits the POST data (https://crbug.com/553614977).
+IN_PROC_BROWSER_TEST_F(RepostFormWarningTest,
+ ConfirmResubmissionAfterBackNavigation) {
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ // Submit a form to an uncacheable URL (/echoall/nocache) with POST data.
+ GURL echo_url = embedded_test_server()->GetURL("/echoall/nocache");
+ {
+ content::TestNavigationObserver observer(web_contents);
+ ASSERT_TRUE(content::ExecJs(
+ web_contents, content::JsReplace(
+ R"(let form = document.createElement('form');
+ form.method = 'POST';
+ form.action = $1;
+ let input = document.createElement('input');
+ input.name = 'text';
+ input.value = 'val';
+ form.appendChild(input);
+ document.body.appendChild(form);
+ form.submit();)",
+ echo_url)));
+ observer.Wait();
+ EXPECT_TRUE(observer.last_navigation_succeeded());
+ }
+ EXPECT_EQ(echo_url, web_contents->GetLastCommittedURL());
+
+ // Verify that the initial POST was successful and body was echoed.
+ EXPECT_EQ(
+ "text=val\n",
+ content::EvalJs(web_contents,
+ "document.getElementsByTagName('pre')[0].innerText;"));
+
+ // Navigate forward to another page.
+ GURL other_url = embedded_test_server()->GetURL("/title2.html");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), other_url));
+ EXPECT_EQ(other_url, web_contents->GetLastCommittedURL());
+
+ // Navigate back to the POST page. Because it is uncacheable, this results in
+ // net::ERR_CACHE_MISS and displays the "Confirm Form Resubmission" error
+ // page.
+ {
+ content::TestNavigationObserver back_observer(web_contents);
+ web_contents->GetController().GoBack();
+ back_observer.Wait();
+ EXPECT_FALSE(back_observer.last_navigation_succeeded());
+ EXPECT_EQ(net::ERR_CACHE_MISS, back_observer.last_net_error_code());
+ }
+ EXPECT_EQ(echo_url, web_contents->GetLastCommittedURL());
+
+ // Reload the page, checking for repost.
+ web_contents->GetController().Reload(content::ReloadType::NORMAL,
+ /*check_for_repost=*/true);
+
+ // Wait for the repost warning dialog to become active.
+ WebContentsModalDialogManager* modal_dialog_manager =
+ WebContentsModalDialogManager::FromWebContents(web_contents);
+ ASSERT_TRUE(base::test::RunUntil(
+ [&]() { return modal_dialog_manager->IsDialogActive(); }));
+
+ // Find the dialog widget.
+ views::Widget* dialog_widget = nullptr;
+ for (views::Widget* widget : views::test::WidgetTest::GetAllWidgets()) {
+ if (widget->widget_delegate() &&
+ widget->widget_delegate()->AsDialogDelegate() &&
+ widget->widget_delegate()->GetWindowTitle() ==
+ l10n_util::GetStringUTF16(IDS_HTTP_POST_WARNING_TITLE)) {
+ dialog_widget = widget;
+ break;
+ }
+ }
+ ASSERT_TRUE(dialog_widget);
+
+ // Accept the dialog ("Continue") and wait for the reload to finish.
+ content::TestNavigationObserver reload_observer(web_contents);
+ views::test::AcceptDialog(dialog_widget);
+ reload_observer.Wait();
+ EXPECT_TRUE(reload_observer.last_navigation_succeeded());
+ EXPECT_EQ(echo_url, web_contents->GetLastCommittedURL());
+
+ // Verify that the reload was a POST request.
+ std::string request_headers =
+ content::EvalJs(web_contents,
+ "document.getElementById('request-headers').innerText;")
+ .ExtractString();
+ EXPECT_THAT(request_headers, ::testing::HasSubstr("POST /echoall/nocache"));
+
+ // Verify that the POST body was resubmitted and echoed in the page body.
+ EXPECT_EQ(
+ "text=val\n",
+ content::EvalJs(web_contents,
+ "document.getElementsByTagName('pre')[0].innerText;"));
+}
diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc
index b7e23d3..74daa15 100644
--- a/content/browser/renderer_host/navigation_request.cc
+++ b/content/browser/renderer_host/navigation_request.cc
@@ -3544,10 +3544,21 @@
origin_related_state_.reset();
// If this was not a redirect that preserves POST submissions (e.g., 307), or
- // if this will be an error page that may end up in another process, then
- // clear the post_data as well to prevent leaking file references to a
- // different SiteInstance.
- if (!IsPost() || DidEncounterError()) {
+ // if this will be a blocked error page or otherwise ends up in the current
+ // process, then clear the post_data as well to prevent leaking file
+ // references to a different SiteInstance.
+ //
+ // It is not necessary to reset POST data for error pages in the isolated
+ // error page process, and it is important not to reset it for failed cases
+ // that end up in kDestinationProcess (e.g. "Confirm Form Resubmission"
+ // pages with ERR_CACHE_MISS), when it might later be used successfully.
+ //
+ // TODO(crbug.com/40134629): Remove the error page exception if subframe error
+ // page isolation is enabled.
+ if (!IsPost() ||
+ (DidEncounterError() &&
+ (ComputeErrorPageProcess() == ErrorPageProcess::kCurrentProcess ||
+ net::IsRequestBlockedError(net_error_)))) {
common_params_->post_data.reset();
}
}
Regression Test / PoC
diff --git a/chrome/browser/repost_form_warning_browsertest.cc b/chrome/browser/repost_form_warning_browsertest.cc
index d7ce5b7..425e2300 100644
--- a/chrome/browser/repost_form_warning_browsertest.cc
+++ b/chrome/browser/repost_form_warning_browsertest.cc
@@ -11,14 +11,22 @@
#include "chrome/common/url_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
+#include "components/strings/grit/components_strings.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
+#include "net/base/net_errors.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "ui/base/l10n/l10n_util.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
+#include "ui/views/test/dialog_test.h"
+#include "ui/views/test/widget_test.h"
+#include "ui/views/widget/widget.h"
using web_modal::WebContentsModalDialogManager;
@@ -121,3 +129,98 @@
ShowAndVerifyUi();
}
#endif
+
+// Verifies that confirming form resubmission after navigating back to an
+// uncacheable POST page resubmits the POST data (https://crbug.com/553614977).
+IN_PROC_BROWSER_TEST_F(RepostFormWarningTest,
+ ConfirmResubmissionAfterBackNavigation) {
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ // Submit a form to an uncacheable URL (/echoall/nocache) with POST data.
+ GURL echo_url = embedded_test_server()->GetURL("/echoall/nocache");
+ {
+ content::TestNavigationObserver observer(web_contents);
+ ASSERT_TRUE(content::ExecJs(
+ web_contents, content::JsReplace(
+ R"(let form = document.createElement('form');
+ form.method = 'POST';
+ form.action = $1;
+ let input = document.createElement('input');
+ input.name = 'text';
+ input.value = 'val';
+ form.appendChild(input);
+ document.body.appendChild(form);
+ form.submit();)",
+ echo_url)));
+ observer.Wait();
+ EXPECT_TRUE(observer.last_navigation_succeeded());
+ }
+ EXPECT_EQ(echo_url, web_contents->GetLastCommittedURL());
+
+ // Verify that the initial POST was successful and body was echoed.
+ EXPECT_EQ(
+ "text=val\n",
+ content::EvalJs(web_contents,
+ "document.getElementsByTagName('pre')[0].innerText;"));
+
+ // Navigate forward to another page.
+ GURL other_url = embedded_test_server()->GetURL("/title2.html");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), other_url));
+ EXPECT_EQ(other_url, web_contents->GetLastCommittedURL());
+
+ // Navigate back to the POST page. Because it is uncacheable, this results in
+ // net::ERR_CACHE_MISS and displays the "Confirm Form Resubmission" error
+ // page.
+ {
+ content::TestNavigationObserver back_observer(web_contents);
+ web_contents->GetController().GoBack();
+ back_observer.Wait();
+ EXPECT_FALSE(back_observer.last_navigation_succeeded());
+ EXPECT_EQ(net::ERR_CACHE_MISS, back_observer.last_net_error_code());
+ }
+ EXPECT_EQ(echo_url, web_contents->GetLastCommittedURL());
+
+ // Reload the page, checking for repost.
+ web_contents->GetController().Reload(content::ReloadType::NORMAL,
+ /*check_for_repost=*/true);
+
+ // Wait for the repost warning dialog to become active.
+ WebContentsModalDialogManager* modal_dialog_manager =
+ WebContentsModalDialogManager::FromWebContents(web_contents);
+ ASSERT_TRUE(base::test::RunUntil(
+ [&]() { return modal_dialog_manager->IsDialogActive(); }));
+
+ // Find the dialog widget.
+ views::Widget* dialog_widget = nullptr;
+ for (views::Widget* widget : views::test::WidgetTest::GetAllWidgets()) {
+ if (widget->widget_delegate() &&
+ widget->widget_delegate()->AsDialogDelegate() &&
+ widget->widget_delegate()->GetWindowTitle() ==
+ l10n_util::GetStringUTF16(IDS_HTTP_POST_WARNING_TITLE)) {
+ dialog_widget = widget;
+ break;
+ }
+ }
+ ASSERT_TRUE(dialog_widget);
+
+ // Accept the dialog ("Continue") and wait for the reload to finish.
+ content::TestNavigationObserver reload_observer(web_contents);
+ views::test::AcceptDialog(dialog_widget);
+ reload_observer.Wait();
+ EXPECT_TRUE(reload_observer.last_navigation_succeeded());
+ EXPECT_EQ(echo_url, web_contents->GetLastCommittedURL());
+
+ // Verify that the reload was a POST request.
+ std::string request_headers =
+ content::EvalJs(web_contents,
+ "document.getElementById('request-headers').innerText;")
+ .ExtractString();
+ EXPECT_THAT(request_headers, ::testing::HasSubstr("POST /echoall/nocache"));
+
+ // Verify that the POST body was resubmitted and echoed in the page body.
+ EXPECT_EQ(
+ "text=val\n",
+ content::EvalJs(web_contents,
+ "document.getElementsByTagName('pre')[0].innerText;"));
+}
Original Bug Report
Potential Cross-Origin File Leak to Non-Target Process via Uncleared post_data in FailedNavigations
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: A missing cleanup in NavigationRequest::ResetStateForSiteInstanceChange() allows a compromised renderer to obtain GrantReadFile permissions for local files previously uploaded to cross-origin sites. By triggering a blocked history navigation in a subframe, the attacker forces the browser to grant file access to the wrong process and leak the target file path via IPC.
Affected files:
content/browser/renderer_host/render_frame_host_impl.cccontent/browser/renderer_host/navigation_request.cccontent/browser/renderer_host/render_frame_host_manager.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
1. Summary of the Issue (Meant for Human Triage)
A severe potential cross-origin file read vulnerability exists in Chromium’s navigation stack due to a failure to clear POST request body state during navigation-state resets on failed navigations. When a renderer-initiated history subframe navigation is blocked (for example, by a Content Security Policy or a NavigationThrottle), NavigationRequest::ComputeErrorPageProcess() determines that the error page should be hosted in the current frame’s process (kCurrentProcess). Consequently, FailedNavigation is invoked on the current process’s RenderFrameHost, which calls UpdatePermissionsForNavigation to grant permissions to that process.
If the current process (Process A, locked to Site A) differs from the original destination process (Process B, locked to Site B), ResetStateForSiteInstanceChange() is triggered. While this correctly mitigates serialized PageState-based file access by clearing commit_params_->page_state, it critically fails to clear common_params_->post_data. As a result, the local file paths embedded in the post_data—which the user legitimately uploaded to Site B via a file picker in a prior session—are passed to GrantFileAccessFromResourceRequestBody. This issues a GrantReadFile permission to the attacker’s Process A.
Furthermore, the exact local file path is sent to the attacker’s process via the CommitFailedNavigation Mojo IPC. This allows a compromised renderer hosting Site A to bypass downstream CanReadFile checks and silently exfiltrate arbitrary files previously selected by the user on Site B.
2. Proof-of-Concept & Detailed Execution Flow
Note: Our tooling agent does not have the ability to run code. The following are potential, suggested steps an attacker would follow to trigger the vulnerability based on static codebase analysis.
Attacker Setup & Preconditions
- Compromised Target: The attacker controls a malicious website (Site A) and has compromised the renderer process hosting it (Process P_A).
- Target File Upload: The attacker embeds a cross-origin iframe to the victim site (Site B). The user interacts with Site B (e.g., via
<input type="file">) and uploads a local file (File F). - History State: The browser creates a
FrameNavigationEntry(FNE_B) for Site B. Since the form was submitted via POST,FNE_B->GetPostData()stores aResourceRequestBodycontaining aDataElementFilereferencing File F.
Potential Execution Trace
- Return to Site A: The compromised Process P_A navigates the iframe back to a same-site URL (Site A). The iframe’s active
RenderFrameHostis now hosted in Process P_A, but the joint history session retains FNE_B. - Block Policy Injection: Process P_A injects a Content Security Policy into its main frame that blocks subframe navigations to Site B (e.g.,
frame-src 'self'). - Trigger History Back: Process P_A initiates a history back navigation targeting the Site B entry (
LocalMainFrameHost::GoToEntryAtOffset(-1)). - Request Construction:
NavigationControllerImpl::CreateNavigationRequestFromEntry(content/browser/renderer_host/navigation_controller_impl.cc:4866-5006) extracts thepost_dataviaframe_entry->GetPostData()and assigns it to the newNavigationRequest’scommon_params->post_dataintact. - Navigation Blocked:
NavigationRequest::BeginNavigationImplevaluates the CSP, which blocks the navigation, yieldingERR_BLOCKED_BY_CSP. - Error Process Assignment:
NavigationRequest::SelectFrameHostForOnRequestFailedInternalcallsComputeErrorPageProcess()(navigation_request.cc:5814-5856). Because it is a subframe (IsErrorPageIsolationEnabled()evaluates tofalse), it is a blocked request (IsRequestBlockedError() == true), and it is renderer-initiated (!browser_initiated()), it explicitly returnsErrorPageProcess::kCurrentProcess(Process P_A). - State Reset Failure:
GetSiteInstanceForNavigationRequestassigns the error navigation to Site A’sSiteInstance. Because the destinationSiteInstanceof the history entry (Site B) differs from the assigned instance (Site A),NavigationRequest::ResetStateForSiteInstanceChange()is called (navigation_request.cc:3636-3661). This function resets bindings and clearscommit_params_->page_state, but fails to clear or resetcommon_params_->post_data. - Unauthorized File Grant:
NavigationRequest::CommitErrorPagecallsGetRenderFrameHost()->FailedNavigation()on Process P_A’sRenderFrameHost, which in turn invokesUpdatePermissionsForNavigation(render_frame_host_impl.cc:14048). - Sink Execution:
Because
// content/browser/renderer_host/render_frame_host_impl.cc:14092-14094 if (request->common_params().post_data) { GrantFileAccessFromResourceRequestBody(*request->common_params().post_data); }post_datasurvived the reset, this iterates overDataElementFileand invokesChildProcessSecurityPolicyImpl::GrantReadFile, permanently granting Process P_A (Site A) read access to File F. - Data Leakage & Exfiltration: The browser sends the
CommitFailedNavigationMojo IPC to Process P_A. As defined incontent/common/navigation_client.mojom, this IPC includesCommonNavigationParams. Inservices/network/public/cpp/url_request_mojom_traits.cc, the renderer deserializes theURLRequestBodyandDataElementFile, placing the exact local file path string directly into the attacker’s memory space. Process P_A can now initiate a newfetch()using the leaked path, successfully bypassing all browserCanReadFilechecks.
Suggested Fix
In content/browser/renderer_host/navigation_request.cc, inside NavigationRequest::ResetStateForSiteInstanceChange(), explicitly clear common_params_->post_data alongside commit_params_->page_state.
if (page_state.IsValid())
commit_params_->page_state =
blink::PageState::CreateFromURL(GetURL()).ToEncodedData();
+ // Also clear post_data to prevent leaking cross-origin file references or
+ // issuing unauthorized file grants to the wrong process.
+ common_params_->post_data = nullptr;
3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)
Prior Critic Verdict
> Severity: High (S1)
> Brief Notes / Reasoning:
> The report describes a valid cross-origin file leak via ComputeErrorPageProcess and UpdatePermissionsForNavigation. When a renderer-initiated history navigation for a subframe is blocked (e.g. via CSP injected by a compromised renderer), ComputeErrorPageProcess() evaluates to kCurrentProcess (process P_A, locked to site A). FailedNavigation then invokes UpdatePermissionsForNavigation(request) on P_A’s RFH.
>
> UpdatePermissionsForNavigation calls GrantFileAccessFromResourceRequestBody with request->common_params().post_data. Because ResetStateForSiteInstanceChange() fails to clear post_data, the file paths in the post_data—which the user uploaded to site B in a prior interaction—are passed to GrantReadFile, granting P_A access to those files. Once P_A legitimately holds the GrantReadFile permission for file F, it can freely bypass downstream CanReadFile checks and exfiltrate the file to site A.
>
> The rewriter downrated this to Low (S3), claiming it duplicates a ’low’ finding and non-arbitrary file reads don’t meet the S1 bar. However, the severity guidelines explicitly state: ‘A ChildProcessSecurityPolicy grant for site B or file:// is issued to a process locked to site A’ is High (S1) severity. Furthermore, this constitutes an ability to ‘read cross-origin data’ (a file intended for another origin), which is also S1. The prerequisite user interaction (uploading a file via a file picker) is a standard web interaction, not ‘unusual or complex’, and does not warrant a severity downgrade.
Additional Agent Verifications & Logic Proofs
- IPC Serialization / Deserialization Confirmations:
- Verified via codebase tracing that
content/common/navigation_client.mojom:397-411definesCommitFailedNavigationtakingblink.mojom.CommonNavigationParams common_params. third_party/blink/public/mojom/navigation/navigation_params.mojom:273confirmsCommonNavigationParamscontainsnetwork.mojom.URLRequestBody? post_data.services/network/public/cpp/url_request_mojom_traits.cc:229-242confirms thatDataElementFiledeserializes the file path directly into the renderer process’s memory space intact (base::FilePath path), without any stripping.
- Verified via codebase tracing that
- Subframe Error Isolation Evaluation:
- Verified
FrameTreeNode::IsErrorPageIsolationEnabled(lines 1140-1143) routes toSiteIsolationPolicy::IsErrorPageIsolationEnabled(IsMainFrame()). - Traced to
ChromeContentBrowserClient::ShouldIsolateErrorPage(bool in_main_frame)(lines 2551-2555) which strictly returnsin_main_frame. For subframes, this is deterministicallyfalse.
- Verified
- Missing Cleanup Validation:
- Codebase audit of
NavigationRequest::ResetStateForSiteInstanceChange(navigation_request.cc:3636-3661) confirms it exclusively resetsbindings_,commit_params_->page_state, andorigin_related_state_.common_params_->post_dataremains completely unaddressed.
- Codebase audit of
- Data Preservation in Navigation Generation:
- Codebase audit of
NavigationControllerImpl::CreateNavigationRequestFromEntry(navigation_controller_impl.cc:4866-5006) confirmsrequest_bodyis extracted fromframe_entry->GetPostData()and placed un-sanitized intoentry->ConstructCommonNavigationParams().
- Codebase audit of
Evaluated with Chrome root at commit: b25acdb3da6209f69a155cbd1dbbaca3c21f535b
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.