High watch CVSS 8.3 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Navigation
DescriptionInappropriate implementation in Navigation
ComponentNavigation
Bug ClassLogic Error
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker531165110
Fix commit202324afa3f4 (chromium/src) +118/-4
CWECWE-693
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-07-04 (per vendor advisory)
Fix landed2026-09-03 (public git)
Disclosed2026-08-06

Background

`common_params_->post_data`
the request body attached to a NavigationRequest; per the code comment it can carry file references, so it must not be sent to a renderer belonging to a different SiteInstance.
`ComputeErrorPageProcess()` / `ErrorPageProcess`
decides where a failed navigation’s error page commits: kCurrentProcess (the current frame’s renderer), the isolated error-page process, or kDestinationProcess (the process for the target URL).
`net::IsRequestBlockedError()`
identifies “blocked” network errors, whose error pages are, per the new comment, the other case where POST data could reach the wrong renderer.
Confirm Form Resubmission
the ERR_CACHE_MISS error page shown when going back to an uncacheable POST result; reloading it with check_for_repost re-sends the stored POST body after the user confirms.

Root Cause Analysis

The patched block, in a NavigationRequest method whose name is not visible in the hunk (it runs next to origin_related_state_.reset() when request state is reset, such as on redirects), decides when to drop common_params_->post_data. Before the patch, the condition was !IsPost() || DidEncounterError(), so every failed navigation lost its POST body. The goal was to stop file references from leaking to an error page that “may end up in another process”.

The patch relaxes this: on error, the POST body is now cleared only when ComputeErrorPageProcess() == ErrorPageProcess::kCurrentProcess or net::IsRequestBlockedError(net_error_). It is kept for error pages in the isolated error-page process and for kDestinationProcess failures such as ERR_CACHE_MISS. The real security invariant, spelled out in the new comment, is narrower than “any error”: POST data must not reach the renderer of the current page or a blocked error page, since those are the cases where it could land in a different SiteInstance. The commit’s main effect is functional, restoring the “Confirm Form Resubmission” flow (crbug.com/553614977, which the regression test exercises). For the security bug 531165110, it keeps the necessary clearing while dropping the over-broad part. The diff does not show the earlier change that introduced the DidEncounterError() clearing, so how this exact commit relates to the CVE can only be inferred from the shared bug ID.

Key insight
POST bodies, which can carry file references, have to be withheld from a renderer that does not own the destination SiteInstance. That risk exists only when the error page commits in the current process or the navigation was blocked, not for every network error. The fix ties the clearing decision to ComputeErrorPageProcess() and IsRequestBlockedError(), so the cross-SiteInstance cases stay protected and retryable kDestinationProcess failures keep their POST data.

Attack Path

  1. POST navigation with a body A frame navigates with POST data that may contain file references, such as an uploaded file in a form submission (the file-reference concern comes from the code comment).
  2. Navigation fails The request ends in a network error, so an error page is committed instead of the target document.
  3. Error page lands in the wrong process If the error page commits in ErrorPageProcess::kCurrentProcess (the comment says subframe error pages are not yet isolated, crbug.com/40134629) or is a blocked-response error, the navigation’s post_data could be handed to a renderer whose SiteInstance is not the destination’s.
  4. Renderer gains access to POST contents That renderer could receive the POST body or the file references within it, which it should not have (inferred, since the diff does not show the downstream file-grant handling).

Impact Assessment

The bug class is a logic/policy error in browser-process navigation code. The worst case is that a renderer for a different site gets another site’s POST data or file references, which is cross-site information disclosure (inferred from the “leaking file references to a different SiteInstance” comment). The leaky cases require a failed POST navigation whose error page commits in the current process (for example an unisolated subframe error page) or a blocked-response error, so it depends on a cross-site embedding or navigation setup rather than a single page load. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. The direct effect of this specific commit is to restore form resubmission after ERR_CACHE_MISS.

Files Changed

  • content/browser/renderer_host/navigation_request.cc
  • chrome/browser/repost_form_warning_browsertest.cc

Audit Directions

  • Request bodies across process transitions
    Audit every path where post_data, ResourceRequestBody or file-backed blobs travel with a navigation that may commit in a different process (redirects, error pages, about:blank fallbacks, prerender/BFCache activation). Check that the body is dropped or the access grant is scoped to the destination SiteInstance.
  • Error-page process selection
    Wherever ComputeErrorPageProcess() returns kCurrentProcess (subframes without error-page isolation), look for other navigation state (headers, initiator, origin-related state) that can be passed to the wrong renderer.
  • Over-broad mitigations that get narrowed
    When a security clear-out is later narrowed for compatibility, as here, check the narrowed condition against every error code and process outcome so that a blocked or cross-site case is not quietly reopened.
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/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();
   }
 }
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;"));
+}
Loading diff…

Regression Test / PoC

shipped with the fix
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;"));
+}
Loading diff…

Original Bug Report

reported by aw...@chromium.org

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.cc
  • content/browser/renderer_host/navigation_request.cc
  • content/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

  1. Compromised Target: The attacker controls a malicious website (Site A) and has compromised the renderer process hosting it (Process P_A).
  2. 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).
  3. History State: The browser creates a FrameNavigationEntry (FNE_B) for Site B. Since the form was submitted via POST, FNE_B->GetPostData() stores a ResourceRequestBody containing a DataElementFile referencing File F.

Potential Execution Trace

  1. Return to Site A: The compromised Process P_A navigates the iframe back to a same-site URL (Site A). The iframe’s active RenderFrameHost is now hosted in Process P_A, but the joint history session retains FNE_B.
  2. 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').
  3. Trigger History Back: Process P_A initiates a history back navigation targeting the Site B entry (LocalMainFrameHost::GoToEntryAtOffset(-1)).
  4. Request Construction: NavigationControllerImpl::CreateNavigationRequestFromEntry (content/browser/renderer_host/navigation_controller_impl.cc:4866-5006) extracts the post_data via frame_entry->GetPostData() and assigns it to the new NavigationRequest’s common_params->post_data intact.
  5. Navigation Blocked: NavigationRequest::BeginNavigationImpl evaluates the CSP, which blocks the navigation, yielding ERR_BLOCKED_BY_CSP.
  6. Error Process Assignment: NavigationRequest::SelectFrameHostForOnRequestFailedInternal calls ComputeErrorPageProcess() (navigation_request.cc:5814-5856). Because it is a subframe (IsErrorPageIsolationEnabled() evaluates to false), it is a blocked request (IsRequestBlockedError() == true), and it is renderer-initiated (!browser_initiated()), it explicitly returns ErrorPageProcess::kCurrentProcess (Process P_A).
  7. State Reset Failure: GetSiteInstanceForNavigationRequest assigns the error navigation to Site A’s SiteInstance. Because the destination SiteInstance of 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 clears commit_params_->page_state, but fails to clear or reset common_params_->post_data.
  8. Unauthorized File Grant: NavigationRequest::CommitErrorPage calls GetRenderFrameHost()->FailedNavigation() on Process P_A’s RenderFrameHost, which in turn invokes UpdatePermissionsForNavigation (render_frame_host_impl.cc:14048).
  9. Sink Execution:
    // content/browser/renderer_host/render_frame_host_impl.cc:14092-14094
    if (request->common_params().post_data) {
      GrantFileAccessFromResourceRequestBody(*request->common_params().post_data);
    }
    
    Because post_data survived the reset, this iterates over DataElementFile and invokes ChildProcessSecurityPolicyImpl::GrantReadFile, permanently granting Process P_A (Site A) read access to File F.
  10. Data Leakage & Exfiltration: The browser sends the CommitFailedNavigation Mojo IPC to Process P_A. As defined in content/common/navigation_client.mojom, this IPC includes CommonNavigationParams. In services/network/public/cpp/url_request_mojom_traits.cc, the renderer deserializes the URLRequestBody and DataElementFile, placing the exact local file path string directly into the attacker’s memory space. Process P_A can now initiate a new fetch() using the leaked path, successfully bypassing all browser CanReadFile checks.

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-411 defines CommitFailedNavigation taking blink.mojom.CommonNavigationParams common_params.
    • third_party/blink/public/mojom/navigation/navigation_params.mojom:273 confirms CommonNavigationParams contains network.mojom.URLRequestBody? post_data.
    • services/network/public/cpp/url_request_mojom_traits.cc:229-242 confirms that DataElementFile deserializes the file path directly into the renderer process’s memory space intact (base::FilePath path), without any stripping.
  • Subframe Error Isolation Evaluation:
    • Verified FrameTreeNode::IsErrorPageIsolationEnabled (lines 1140-1143) routes to SiteIsolationPolicy::IsErrorPageIsolationEnabled(IsMainFrame()).
    • Traced to ChromeContentBrowserClient::ShouldIsolateErrorPage(bool in_main_frame) (lines 2551-2555) which strictly returns in_main_frame. For subframes, this is deterministically false.
  • Missing Cleanup Validation:
    • Codebase audit of NavigationRequest::ResetStateForSiteInstanceChange (navigation_request.cc:3636-3661) confirms it exclusively resets bindings_, commit_params_->page_state, and origin_related_state_. common_params_->post_data remains completely unaddressed.
  • Data Preservation in Navigation Generation:
    • Codebase audit of NavigationControllerImpl::CreateNavigationRequestFromEntry (navigation_controller_impl.cc:4866-5006) confirms request_body is extracted from frame_entry->GetPostData() and placed un-sanitized into entry->ConstructCommonNavigationParams().

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.

View on issue tracker