Medium chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Navigation
DescriptionInformation leak in Navigation
ComponentNavigation
Bug ClassLogic Error
Tracker517156678
Fix commitc600e0a60ac0 (chromium/src) +327/-24
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/navigation_request.cc
modified
for
content/browser/renderer_host/navigation_request.cc
modified

Files Changed

  • chrome/browser/extensions/process_manager_browsertest.cc
  • chrome/browser/subresource_filter/subresource_filter_browsertest.cc
  • chrome/browser/subresource_filter/subresource_filter_fenced_frame_browsertest.cc
  • content/browser/renderer_host/navigation_request.cc
From c600e0a60ac08d7485c6f4c0100d104ac3503d80 Mon Sep 17 00:00:00 2001
From: Camillia Smith Barnes <[email protected]>
Date: Mon, 10 Aug 2026 18:06:00 -0700
Subject: [PATCH] Navigation: Sanitize error page final URL committed in current process

SanitizeRedirectsForCommit() preserves the last redirect_infos entry and
common_params->url on the assumption that the renderer receiving them is
the one committing that URL. That does not hold for CommitErrorPage()
when ComputeErrorPageProcess() returns kCurrentProcess
(renderer-initiated navigation blocked after a redirect, subframe error
isolation off): the error page commits in the initiator's process, which
may be cross-origin to the post-redirect URL.

When committing an error page in kCurrentProcess after a redirect, also
reduce common_params_->url and the last redirect_infos entry to their
origin, behind the new kSanitizeFailedSubframeNavigationUrls feature.

Notes on Potential Regressions: Because this changes the URL that claims
to be committing, it introduces a few minor behavioral
changes/regressions:
* Error Page Strings: Some strings shown in error pages that usually
depend on the full URL will now display only the origin instead of the
full path.
* "Reload Frame" Context Menu: Subframe reloads via the "Reload Frame"
context menu option might break or stop working (e.g., if a developer
modifies a CSP that caused the block and attempts to reload just the
frame).
* Embedded Reload Buttons: Reload buttons embedded directly in error
pages could break, though these are generally hidden in subframes.

These minor regressions are considered an acceptable trade-off for the
security benefits of preventing redirect URL leaks. This change acts as
a bandaid and the regressions will be resolved when Subframe Error Page
Isolation (crbug.com/40134629) ships. The
`kSanitizeFailedSubframeNavigationUrls` feature flag acts as a kill
switch in the event of major breakage in the wild.

Bug: 517156678
Change-Id: I11f4869923b517e899fc56d2b6d4ba420ef28c53
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8178743
Reviewed-by: Charlie Reis <[email protected]>
Commit-Queue: Cammie Smith Barnes <[email protected]>
Reviewed-by: Alex Moshchuk <[email protected]>
Reviewed-by: Josh Karlin <[email protected]>
Reviewed-by: Kelvin Jiang <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1676871}
---

diff --git a/chrome/browser/extensions/process_manager_browsertest.cc b/chrome/browser/extensions/process_manager_browsertest.cc
index 985e218b..6733a6a 100644
--- a/chrome/browser/extensions/process_manager_browsertest.cc
+++ b/chrome/browser/extensions/process_manager_browsertest.cc
@@ -1673,9 +1673,12 @@
         << "The initial navigation should be allowed, but not the server "
            "redirect to extension2's manifest";
     EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, nav_observer.last_net_error_code());
-    EXPECT_EQ(extension2_manifest, nav_observer.last_navigation_url());
-    EXPECT_EQ(extension2_manifest,
-              ChildFrameAt(main_frame, 1)->GetLastCommittedURL());
+    // We expect the URL to be sanitized, per https://crbug.com/517156678.
+    // TODO(crbug.com/40134629): Remove the sanitization once Subframe Error
+    // Page Isolation ships.
+    GURL expected_url = extension2_manifest.DeprecatedGetOriginAsURL();
+    EXPECT_EQ(expected_url, nav_observer.last_navigation_url());
+    EXPECT_EQ(expected_url, ChildFrameAt(main_frame, 1)->GetLastCommittedURL());
     EXPECT_EQ(1u, pm->GetAllFrames().size());
     EXPECT_EQ(1u, pm->GetRenderFrameHostsForExtension(extension1->id()).size());
     EXPECT_EQ(0u, pm->GetRenderFrameHostsForExtension(extension2->id()).size());
diff --git a/chrome/browser/subresource_filter/subresource_filter_browsertest.cc b/chrome/browser/subresource_filter/subresource_filter_browsertest.cc
index 15c1c9a..403cc30 100644
--- a/chrome/browser/subresource_filter/subresource_filter_browsertest.cc
+++ b/chrome/browser/subresource_filter/subresource_filter_browsertest.cc
@@ -383,7 +383,11 @@
 
   content::RenderFrameHost* frame = FindFrameByName(kSubframeNames[0]);
   ASSERT_TRUE(frame);
-  EXPECT_EQ(disallowed_subdocument_url, frame->GetLastCommittedURL());
+  // We expect the URL to be sanitized, per https://crbug.com/517156678.
+  // TODO(crbug.com/40134629): Remove the sanitization once Subframe Error Page
+  // Isolation ships.
+  EXPECT_EQ(disallowed_subdocument_url.DeprecatedGetOriginAsURL(),
+            frame->GetLastCommittedURL());
   ExpectFramesIncludedInLayout(kSubframeNames, kExpectOnlySecondSubframe);
 }
 
diff --git a/chrome/browser/subresource_filter/subresource_filter_fenced_frame_browsertest.cc b/chrome/browser/subresource_filter/subresource_filter_fenced_frame_browsertest.cc
index 36d4826..ffc1ccec 100644
--- a/chrome/browser/subresource_filter/subresource_filter_fenced_frame_browsertest.cc
+++ b/chrome/browser/subresource_filter/subresource_filter_fenced_frame_browsertest.cc
@@ -273,7 +273,11 @@
 
   EXPECT_FALSE(WasParsedScriptElementLoaded(subframe));
   EXPECT_TRUE(subframe->IsErrorDocument());
-  EXPECT_EQ(kUrlWithIncludedScript, subframe->GetLastCommittedURL());
+  // We expect the URL to be sanitized, per https://crbug.com/517156678.
+  // TODO(crbug.com/40134629): Remove the sanitization once Subframe Error Page
+  // Isolation ships.
+  GURL expected_url = url::Origin::Create(kUrlWithIncludedScript).GetURL();
+  EXPECT_EQ(expected_url, subframe->GetLastCommittedURL());
 }
 
 }  // namespace subresource_filter
diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc
index f55259a4..4c1b80e 100644
--- a/content/browser/renderer_host/navigation_request.cc
+++ b/content/browser/renderer_host/navigation_request.cc
@@ -6818,7 +6818,7 @@
   // the redirect URLs to avoid leaking potentially sensitive data into
   // processes which are cross-site. There is no dependency on the
   // cross-site-ness, therefore just sanitize unilaterally.
-  SanitizeRedirectsForCommit(commit_params_);
+  SanitizeRedirectsForCommit(common_params_, commit_params_);
 
   GetRenderFrameHost()->FailedNavigation(
       this, *common_params_, *commit_params_, has_stale_copy_in_cache_,
@@ -7277,7 +7277,7 @@
   // the redirect URLs to avoid leaking potentially sensitive data into
   // processes which are cross-site. There is no dependency on the
   // cross-site-ness, therefore just sanitize unilaterally.
-  SanitizeRedirectsForCommit(commit_params);
+  SanitizeRedirectsForCommit(common_params, commit_params);
 
   GetRenderFrameHost()->CommitNavigation(
       this, std::move(common_params), std::move(commit_params),
@@ -8424,10 +8424,26 @@
 }
 
 void NavigationRequest::SanitizeRedirectsForCommit(
+    blink::mojom::CommonNavigationParamsPtr& common_params,
     blink::mojom::CommitNavigationParamsPtr& commit_params) {
   if (!base::FeatureList::IsEnabled(kSanitizeRedirectUrlsDuringNavigation)) {
     return;
   }
+
+  // TODO(crbug.com/40134629): Remove the sanitization once Subframe Error
+  // Pages are isolated.
+  const bool should_sanitize_final_url_for_error_page =
+      base::FeatureList::IsEnabled(
+          features::kSanitizeFailedSubframeNavigationUrls) &&
+      ComputeErrorPageProcess() == ErrorPageProcess::kCurrentProcess &&
+      !commit_params->redirect_params.empty() &&
+      !url::Origin::Create(common_params->url)
+           .IsSameOriginWith(GetRenderFrameHost()->GetLastCommittedOrigin());
+
+  if (should_sanitize_final_url_for_error_page) {
+    common_params->url = common_params->url.DeprecatedGetOriginAsURL();
+  }
+
   // It is safe to convert GURL to an Origin and back in the code below because
   // we only want to discard the rest of the URL (e.g., path and params). The
   // actual underlying Origin is not needed, which could be inherited or opaque
@@ -8438,36 +8454,51 @@
 
   // In the redirect_params vector, the last entry contains the URL we are going
   // to commit after following all redirects. We should not be sanitizing it, as
-  // we need to commit the real URL as part of the navigation.
+  // we need to commit the real URL as part of the navigation. Make an exception
+  // if the error page commits in the initiator's process and the navigation was
+  // redirected before failing, in which case that final URL may be cross-origin
+  // to the receiving process and must be reduced to origin as well.
   if (!commit_params->redirect_params.empty()) {
-    auto redirect_params_span = base::span(commit_params->redirect_params);
-    for (blink::mojom::NavigationRedirectParamsPtr& redirect :
-         redirect_params_span.first(redirect_params_span.size() - 1)) {
-      redirect->redirect_info.new_url =
-          redirect->redirect_info.new_url.DeprecatedGetOriginAsURL();
+    base::span<blink::mojom::NavigationRedirectParamsPtr> redirect_params_span(
+        commit_params->redirect_params);
+    if (!should_sanitize_final_url_for_error_page) {
+      redirect_params_span =
+          redirect_params_span.first(redirect_params_span.size() - 1);
+    }
+    for (auto& redirect_param : redirect_params_span) {
+      redirect_param->redirect_info.new_url =
+          redirect_param->redirect_info.new_url.DeprecatedGetOriginAsURL();
     }
   }
 
   if (base::FeatureList::IsEnabled(
           features::kSanitizeLocationHeadersDuringNavigation)) {
-    url::Origin final_origin = url::Origin::Create(common_params_->url);
+    // The expected origin of the process that will host the committed document.
+    // We use this to determine if a redirect is cross-origin to the committing
+    // process. For successful navigations, we assume the committing process
+    // will match the origin of the destination URL. For error pages that are
+    // allowed to commit in the current process, we use the current origin of
+    // the RenderFrameHost.
+    const url::Origin expected_commit_process_origin =
+        should_sanitize_final_url_for_error_page
+            ? GetRenderFrameHost()->GetLastCommittedOrigin()
+            : url::Origin::Create(common_params->url);
 
     // Sanitize the "Location" headers for redirects that are cross-origin to
-    // the final committed URL.
+    // the final committed URL (or receiving process for error pages).
     // TODO(crbug.com/495463654): Consider if we need to handle cases that
     // inherit an origin (e.g. about:blank), or if we cross a CSP sandbox
     // boundary where the origin becomes unique/opaque.
-    for (size_t i = 0; i < commit_params->redirect_params.size(); ++i) {
-      auto& response_head = commit_params->redirect_params[i]->response_head;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/extensions/process_manager_browsertest.cc b/chrome/browser/extensions/process_manager_browsertest.cc
index 985e218b..6733a6a 100644
--- a/chrome/browser/extensions/process_manager_browsertest.cc
+++ b/chrome/browser/extensions/process_manager_browsertest.cc
@@ -1673,9 +1673,12 @@
         << "The initial navigation should be allowed, but not the server "
            "redirect to extension2's manifest";
     EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, nav_observer.last_net_error_code());
-    EXPECT_EQ(extension2_manifest, nav_observer.last_navigation_url());
-    EXPECT_EQ(extension2_manifest,
-              ChildFrameAt(main_frame, 1)->GetLastCommittedURL());
+    // We expect the URL to be sanitized, per https://crbug.com/517156678.
+    // TODO(crbug.com/40134629): Remove the sanitization once Subframe Error
+    // Page Isolation ships.
+    GURL expected_url = extension2_manifest.DeprecatedGetOriginAsURL();
+    EXPECT_EQ(expected_url, nav_observer.last_navigation_url());
+    EXPECT_EQ(expected_url, ChildFrameAt(main_frame, 1)->GetLastCommittedURL());
     EXPECT_EQ(1u, pm->GetAllFrames().size());
     EXPECT_EQ(1u, pm->GetRenderFrameHostsForExtension(extension1->id()).size());
     EXPECT_EQ(0u, pm->GetRenderFrameHostsForExtension(extension2->id()).size());
diff --git a/chrome/browser/subresource_filter/subresource_filter_browsertest.cc b/chrome/browser/subresource_filter/subresource_filter_browsertest.cc
index 15c1c9a..403cc30 100644
--- a/chrome/browser/subresource_filter/subresource_filter_browsertest.cc
+++ b/chrome/browser/subresource_filter/subresource_filter_browsertest.cc
@@ -383,7 +383,11 @@
 
   content::RenderFrameHost* frame = FindFrameByName(kSubframeNames[0]);
   ASSERT_TRUE(frame);
-  EXPECT_EQ(disallowed_subdocument_url, frame->GetLastCommittedURL());
+  // We expect the URL to be sanitized, per https://crbug.com/517156678.
+  // TODO(crbug.com/40134629): Remove the sanitization once Subframe Error Page
+  // Isolation ships.
+  EXPECT_EQ(disallowed_subdocument_url.DeprecatedGetOriginAsURL(),
+            frame->GetLastCommittedURL());
   ExpectFramesIncludedInLayout(kSubframeNames, kExpectOnlySecondSubframe);
 }
diff --git a/chrome/browser/subresource_filter/subresource_filter_fenced_frame_browsertest.cc b/chrome/browser/subresource_filter/subresource_filter_fenced_frame_browsertest.cc
index 36d4826..ffc1ccec 100644
--- a/chrome/browser/subresource_filter/subresource_filter_fenced_frame_browsertest.cc
+++ b/chrome/browser/subresource_filter/subresource_filter_fenced_frame_browsertest.cc
@@ -273,7 +273,11 @@
 
   EXPECT_FALSE(WasParsedScriptElementLoaded(subframe));
   EXPECT_TRUE(subframe->IsErrorDocument());
-  EXPECT_EQ(kUrlWithIncludedScript, subframe->GetLastCommittedURL());
+  // We expect the URL to be sanitized, per https://crbug.com/517156678.
+  // TODO(crbug.com/40134629): Remove the sanitization once Subframe Error Page
+  // Isolation ships.
+  GURL expected_url = url::Origin::Create(kUrlWithIncludedScript).GetURL();
+  EXPECT_EQ(expected_url, subframe->GetLastCommittedURL());
 }
 
 }  // namespace subresource_filter
diff --git a/content/browser/renderer_host/navigation_request_unittest.cc b/content/browser/renderer_host/navigation_request_unittest.cc
index 013e09d..0b532ad 100644
--- a/content/browser/renderer_host/navigation_request_unittest.cc
+++ b/content/browser/renderer_host/navigation_request_unittest.cc
@@ -811,8 +811,9 @@
 
   NavigationRequest* request =
       NavigationRequest::From(navigation->GetNavigationHandle());
+  auto common_params = request->common_params().Clone();
   auto commit_params = request->commit_params().Clone();
-  request->SanitizeRedirectsForCommit(commit_params);
+  request->SanitizeRedirectsForCommit(common_params, commit_params);
 
   // redirect_params contains entries for B, C, and D, but not the starting URL.
   // Ensure that the full URL for D is preserved.
@@ -877,9 +878,10 @@
 
   NavigationRequest* request =
       NavigationRequest::From(navigation->GetNavigationHandle());
+  auto common_params = request->common_params().Clone();
   auto commit_params = request->commit_params().Clone();
 
-  request->SanitizeRedirectsForCommit(commit_params);
+  request->SanitizeRedirectsForCommit(common_params, commit_params);
 
   EXPECT_EQ(4u, commit_params->redirect_params.size());
 
@@ -955,9 +957,10 @@
 
   NavigationRequest* request =
       NavigationRequest::From(navigation->GetNavigationHandle());
+  auto common_params = request->common_params().Clone();
   auto commit_params = request->commit_params().Clone();
 
-  request->SanitizeRedirectsForCommit(commit_params);
+  request->SanitizeRedirectsForCommit(common_params, commit_params);
 
   EXPECT_EQ(3u, commit_params->redirect_params.size());
 
@@ -1014,9 +1017,10 @@
 
   NavigationRequest* request =
       NavigationRequest::From(navigation->GetNavigationHandle());
+  auto common_params = request->common_params().Clone();
   auto commit_params = request->commit_params().Clone();
 
-  request->SanitizeRedirectsForCommit(commit_params);
+  request->SanitizeRedirectsForCommit(common_params, commit_params);
 
   EXPECT_EQ(2u, commit_params->redirect_params.size());
 
@@ -1081,6 +1085,251 @@
   EXPECT_EQ(start_url, request->original_url());
 }
 
+// Test that when a redirected subframe navigation is blocked and the resulting
+// error page commits in the initiator's process, the final URL is reduced to
+// its origin in the parameters sent to the renderer. See crbug.com/517156678.
+TEST_F(NavigationRequestTest,
+       SanitizeRedirectsForCommitErrorPageInCurrentProcess) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitWithFeatures(
+      /*enabled_features=*/{features::kSanitizeFailedSubframeNavigationUrls,
+                            features::kSanitizeLocationHeadersDuringNavigation},
+      /*disabled_features=*/{});
+
+  // Commit an initial page so the subframe has a parent document.
+  NavigationSimulator::NavigateAndCommitFromDocument(GURL("https://a.com/"),
+                                                     main_test_rfh());
+  auto* child_frame = static_cast<TestRenderFrameHost*>(
+      content::RenderFrameHostTester::For(main_rfh())->AppendChild("child"));
+
+  const GURL start_url("https://b.com/start?param=1");
+  const GURL final_url("https://c.com/path?param=2");
+  std::unique_ptr<NavigationSimulator> navigation =
+      NavigationSimulator::CreateRendererInitiated(start_url, child_frame);
+  navigation->Start();
+
+  auto headers =
+      base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 302 Found");
+  headers->SetHeader("Location", "https://c.com/path?param=2");
+  navigation->SetRedirectHeaders(headers);
+
+  navigation->Redirect(final_url);
+  navigation->Fail(net::ERR_BLOCKED_BY_CLIENT);
+
+  NavigationRequest* request =
+      NavigationRequest::From(navigation->GetNavigationHandle());
+  ASSERT_EQ(NavigationRequest::ErrorPageProcess::kCurrentProcess,
+            request->ComputeErrorPageProcess());
+
+  // The error page commits in the initiator's process, so the final URL (which
+  // is the post-redirect target) should be reduced to its origin in both the
+  // common and commit params.
+  EXPECT_EQ(GURL("https://c.com/"), request->common_params().url);
+  ASSERT_EQ(1u, request->commit_params().redirect_params.size());
+  EXPECT_EQ(GURL("https://c.com/"),
+            request->commit_params().redirect_params[0]->redirect_info.new_url);
+  ASSERT_EQ(1u, request->commit_params().redirects.size());
+  EXPECT_EQ(GURL("https://b.com/"), request->commit_params().redirects[0]);
+
+  if (base::FeatureList::IsEnabled(
+          features::kSanitizeLocationHeadersDuringNavigation)) {
+    size_t iter = 0;
+    std::optional<std::string_view> location =
+        request->commit_params()
+            .redirect_params[0]
+            ->response_head->headers->EnumerateHeader(&iter, "Location");
+    ASSERT_TRUE(location.has_value());
+    EXPECT_EQ("https://c.com/", location.value());
+  }
+}
+
+TEST_F(
+    NavigationRequestTest,
+    SanitizeRedirectsForCommitErrorPageInCurrentProcess_FinalURLFeatureDisabled) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitWithFeatures(
+      /*enabled_features=*/{features::kSanitizeLocationHeadersDuringNavigation},
+      /*disabled_features=*/{features::kSanitizeFailedSubframeNavigationUrls});
+
+  // Commit an initial page so the subframe has a parent document.
+  NavigationSimulator::NavigateAndCommitFromDocument(GURL("https://a.com/"),
+                                                     main_test_rfh());
+  auto* child_frame = static_cast<TestRenderFrameHost*>(
+      content::RenderFrameHostTester::For(main_rfh())->AppendChild("child"));
+
+  const GURL start_url("https://b.com/start?param=1");
+  const GURL final_url("https://c.com/path?param=2");
+  std::unique_ptr<NavigationSimulator> navigation =
+      NavigationSimulator::CreateRendererInitiated(start_url, child_frame);
+  navigation->Start();
+
+  auto headers =
+      base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 302 Found");
+  headers->SetHeader("Location", "https://c.com/path?param=2");
+  navigation->SetRedirectHeaders(headers);
+
+  navigation->Redirect(final_url);
+  navigation->Fail(net::ERR_BLOCKED_BY_CLIENT);
+
+  NavigationRequest* request =
+      NavigationRequest::From(navigation->GetNavigationHandle());
+  ASSERT_EQ(NavigationRequest::ErrorPageProcess::kCurrentProcess,
+            request->ComputeErrorPageProcess());
+
+  // The feature is disabled, so the final URL should NOT be reduced to its
+  // origin.
+  EXPECT_EQ(final_url, request->common_params().url);
+  ASSERT_EQ(1u, request->commit_params().redirect_params.size());
+  EXPECT_EQ(final_url,
+            request->commit_params().redirect_params[0]->redirect_info.new_url);
+  ASSERT_EQ(1u, request->commit_params().redirects.size());
+  EXPECT_EQ(GURL("https://b.com/"), request->commit_params().redirects[0]);
+
+  // Even if kSanitizeLocationHeadersDuringNavigation is enabled, it should not
+  // sanitize the Location header because sanitize_final_url is false (due to
+  // the disabled feature flag), which makes it use the final URL's origin
+  // (c.com) as target_commit_origin, which is same-origin with the redirect
+  // target (c.com).
+  if (base::FeatureList::IsEnabled(
+          features::kSanitizeLocationHeadersDuringNavigation)) {
+    size_t iter = 0;
+    std::optional<std::string_view> location =
+        request->commit_params()
+            .redirect_params[0]
+            ->response_head->headers->EnumerateHeader(&iter, "Location");
+    ASSERT_TRUE(location.has_value());
+    EXPECT_EQ("https://c.com/path?param=2", location.value());
+  }
+}
+
+TEST_F(NavigationRequestTest,
+       DontSanitizeRedirectsForCommitErrorPageInCurrentProcessSameOrigin) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitWithFeatures(
+      /*enabled_features=*/{features::kSanitizeFailedSubframeNavigationUrls,
+                            features::kSanitizeLocationHeadersDuringNavigation},
+      /*disabled_features=*/{});
+
+  // Commit an initial page so the subframe has a parent document.
+  NavigationSimulator::NavigateAndCommitFromDocument(GURL("https://c.com/"),
+                                                     main_test_rfh());
+  auto* child_frame = static_cast<TestRenderFrameHost*>(
+      content::RenderFrameHostTester::For(main_rfh())->AppendChild("child"));
+
+  const GURL start_url("https://b.com/start?param=1");
+  const GURL final_url("https://c.com/path?param=2");
+  std::unique_ptr<NavigationSimulator> navigation =
+      NavigationSimulator::CreateRendererInitiated(start_url, child_frame);
+  navigation->Start();
+
+  auto headers =
+      base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 302 Found");
+  headers->SetHeader("Location", "https://c.com/path?param=2");
+  navigation->SetRedirectHeaders(headers);
+
+  navigation->Redirect(final_url);
+  navigation->Fail(net::ERR_BLOCKED_BY_CLIENT);
+
+  NavigationRequest* request =
+      NavigationRequest::From(navigation->GetNavigationHandle());
+  ASSERT_EQ(NavigationRequest::ErrorPageProcess::kCurrentProcess,
+            request->ComputeErrorPageProcess());
+
+  // The final URL is same-origin with the receiving process (c.com), so it
+  // should NOT be reduced to its origin.
+  EXPECT_EQ(final_url, request->common_params().url);
+  ASSERT_EQ(1u, request->commit_params().redirect_params.size());
+  EXPECT_EQ(final_url,
+            request->commit_params().redirect_params[0]->redirect_info.new_url);
+  ASSERT_EQ(1u, request->commit_params().redirects.size());
+  EXPECT_EQ(GURL("https://b.com/"), request->commit_params().redirects[0]);
+
+  if (base::FeatureList::IsEnabled(
+          features::kSanitizeLocationHeadersDuringNavigation)) {
+    size_t iter = 0;
+    std::optional<std::string_view> location =
+        request->commit_params()
+            .redirect_params[0]
+            ->response_head->headers->EnumerateHeader(&iter, "Location");
+    ASSERT_TRUE(location.has_value());
+    EXPECT_EQ("https://c.com/path?param=2", location.value());
+  }
+}
+
+// Test that when a subframe navigation with multiple redirects (same-origin to
+// each other, but cross-origin to the main page) is blocked and commits an
+// error page in the initiator's process, all redirect URLs are reduced to
+// origin.
+TEST_F(
+    NavigationRequestTest,
+    SanitizeRedirectsForCommitErrorPageInCurrentProcessMultipleRedirectsSameOriginWithEachOther) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitWithFeatures(
+      /*enabled_features=*/{features::kSanitizeFailedSubframeNavigationUrls,
+                            features::kSanitizeLocationHeadersDuringNavigation},
+      /*disabled_features=*/{});
+
+  // Commit an initial page so the subframe has a parent document (origin A).
+  NavigationSimulator::NavigateAndCommitFromDocument(GURL("https://a.com/"),
+                                                     main_test_rfh());
+  auto* child_frame = static_cast<TestRenderFrameHost*>(
+      content::RenderFrameHostTester::For(main_rfh())->AppendChild("child"));
+
+  const GURL start_url("https://b.com/start?param=1");
+  const GURL url_2("https://b.com/path1?param=2");
... (truncated)
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.