Low chrome Cross Origin 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation loss in CORS
DescriptionInformation loss in CORS
ComponentCORS
Sub-componentOOR-CORS
Bug ClassCross Origin
Tracker513518289
Fix commit344021ca3866 (chromium/src) +1449/-207
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
services/network/cors/cors_url_loader.cc
modified
for
services/network/cors/cors_url_loader.cc
modified
GetStorageAccessStatus
services/network/cors/cors_url_loader.cc
modified

Files Changed

  • services/network/cors/cors_url_loader.h
  • services/network/public/cpp/features.h
  • services/network/cors/cors_url_loader_unittest.cc
  • third_party/blink/web_tests/external/wpt/html/cross-origin-embedder-policy/credentialless/reporting-subresource-corp.https.window.js
  • services/network/url_loader_util_unittest.cc
  • services/network/cors/cors_url_loader_test_util.h
  • services/network/url_loader_util.cc
  • services/network/cors/cors_url_loader.cc
  • services/network/url_loader.cc
  • services/network/cors/cors_url_loader_test_util.cc
  • services/network/url_loader_util.h
  • services/network/cors/preflight_controller.cc
  • services/network/public/cpp/features.cc
From 344021ca3866658522fb56a8a98738e9ba9f905c Mon Sep 17 00:00:00 2001
From: Takashi Toyoshima <toyoshim@chromium.org>
Date: Tue, 08 Sep 2026 01:28:10 -0700
Subject: [PATCH] OOR-CORS: Avoid CorsURLLoader restart on redirect

CorsURLLoader previously restarted requests from scratch via
StartRequest() on redirects requiring CORS or preflights. This destroyed
the underlying URLRequest redirect history, causing Fetch Metadata
headers (e.g. Sec-Fetch-Site) to be incorrectly computed.

This CL introduces base::Feature kAvoidCorsURLLoaderRestartOnRedirect
(enabled by default) to handle redirects in-place via
network_loader_->FollowRedirect() and update headers directly. If a
redirect requires a CORS preflight, CorsURLLoader runs the preflight
asynchronously without tearing down the active loader.

Key changes:
- In-place redirect and preflight handling in CorsURLLoader under
  kAvoidCorsURLLoaderRestartOnRedirect.
- Strict validation of redirected URLs and origin permissions via
  OriginAccessList for credentials and client certificates.
- WHATWG Fetch compliant credential handling across cross-origin
  redirects.
- Parameterized test coverage for both in-place and legacy restart
  paths.

Bug: 513518289
Change-Id: Ic250535f64bf316df656d6cbe29e27cdbac7272a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8278433
Reviewed-by: Kenichi Ishibashi <bashi@chromium.org>
Commit-Queue: Takashi Toyoshima <toyoshim@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1693621}
---
diff --git a/services/network/cors/cors_url_loader.cc b/services/network/cors/cors_url_loader.cc
index 4dce4cf7..e3b45b7 100644
--- a/services/network/cors/cors_url_loader.cc
+++ b/services/network/cors/cors_url_loader.cc
@@ -448,9 +448,29 @@
     return;
   }
 
-  if (new_url && (new_url->DeprecatedGetOriginAsURL() !=
-                  deferred_redirect_url_->DeprecatedGetOriginAsURL())) {
-    NOTREACHED() << "Can only change the URL within the same origin.";
+  // Note: Calling HandleComplete() before mojo::ReportBadMessage() is
+  // intentional and required. HandleComplete() notifies forwarding_client_
+  // of completion and cleanly destroys this loader via delete_callback_.
+  // mojo::ReportBadMessage() operates on thread-local message dispatch
+  // context and does not access `this`. Reversing the order would cause
+  // premature pipe disconnection and break client completion expectations.
+  if (new_url) {
+    if (!new_url->is_valid() ||
+        new_url->scheme() != deferred_redirect_url_->scheme() ||
+        !url::IsSameOriginWith(*new_url, *deferred_redirect_url_)) {
+      HandleComplete(URLLoaderCompletionStatus(net::ERR_INVALID_ARGUMENT));
+      mojo::ReportBadMessage(
+          "CorsURLLoader: cross-origin or cross-scheme new_url in "
+          "FollowRedirect is not permitted");
+      return;
+    }
+    if (new_url->has_username() || new_url->has_password()) {
+      HandleComplete(URLLoaderCompletionStatus(net::ERR_INVALID_ARGUMENT));
+      mojo::ReportBadMessage(
+          "CorsURLLoader: new_url with credentials in FollowRedirect is not "
+          "permitted");
+      return;
+    }
   }
 
   deferred_redirect_url_.reset();
@@ -468,10 +488,13 @@
                                       &forbidden_header)) {
     SCOPED_CRASH_KEY_STRING32("network", "forbidden_sec_header",
                               forbidden_header);
+    // Note: Calling HandleComplete() before mojo::ReportBadMessage() is
+    // intentional and required to fulfill client completion notification
+    // before the pipe teardown.
+    HandleComplete(URLLoaderCompletionStatus(net::ERR_INVALID_ARGUMENT));
     mojo::ReportBadMessage(
         "CorsURLLoader: Forbidden Sec- header from renderer in "
         "FollowRedirect");
-    HandleComplete(URLLoaderCompletionStatus(net::ERR_INVALID_ARGUMENT));
     return;
   }
 
@@ -500,6 +523,7 @@
     return;
   }
 
+  // TODO(crbug.com/556536450): Clean up removed_headers handling.
   for (const auto& name : headers_update_params.removed_headers) {
     request_.headers.RemoveHeader(name);
     request_.cors_exempt_headers.RemoveHeader(name);
@@ -523,6 +547,9 @@
 
   const std::string original_method = std::move(request_.method);
   request_.UpdateOnRedirect(redirect_info_);
+  if (new_url) {
+    request_.url = *new_url;
+  }
 
   // Update isolation_info_ and the shared dictionary storage location if they
   // changed as a result of the redirect for a browser-initiated request (e.g.
@@ -560,36 +587,81 @@
   const bool original_fetch_cors_flag = fetch_cors_flag_;
   SetCorsFlagIfNeeded();
 
-  // We cannot use FollowRedirect for a request with preflight (i.e., when
-  // `fetch_cors_flag_` is true and `NeedsPreflight(request_)` is not nullopt).
-  //
-  // When `original_fetch_cors_flag` is false, `fetch_cors_flag_` is true and
-  // `NeedsPreflight(request)` is nullopt, the net/ implementation won't attach
-  // an "origin" header on redirect, as the original request didn't have one.
-  //
-  // When the request method is changed (due to 302 status code, for example),
-  // the net/ implementation removes the origin header.
-  //
-  // In such cases we need to re-issue a request manually in order to attach the
-  // correct origin header. For "no-cors" requests we rely on redirect logic in
-  // net/ (specifically in net/url_request/redirect_util.cc).
-  //
-  // After both OOR-CORS and network service are fully shipped, we may be able
-  // to remove the logic in net/.
-  if ((fetch_cors_flag_ && NeedsPreflight(request_)) ||
-      (!original_fetch_cors_flag && fetch_cors_flag_) ||
-      (fetch_cors_flag_ && original_method != request_.method)) {
-    DCHECK_NE(request_.mode, mojom::RequestMode::kNoCors);
-    network_client_receiver_.reset();
-    sync_client_receiver_factory_.InvalidateWeakPtrs();
-    StartRequest();
+  if (!base::FeatureList::IsEnabled(
+          features::kAvoidCorsURLLoaderRestartOnRedirect) ||
+      request_.trust_token_params) {
+    // We cannot use FollowRedirect for a request with preflight (i.e., when
+    // `fetch_cors_flag_` is true and `NeedsPreflight(request_)` is not
+    // nullopt).
+    //
+    // When `original_fetch_cors_flag` is false, `fetch_cors_flag_` is true and
+    // `NeedsPreflight(request)` is nullopt, the net/ implementation won't
+    // attach an "origin" header on redirect, as the original request didn't
+    // have one.
+    //
+    // When the request method is changed (due to 302 status code, for example),
+    // the net/ implementation removes the origin header.
+    //
+    // In such cases we need to re-issue a request manually in order to attach
+    // the correct origin header. For "no-cors" requests we rely on redirect
+    // logic in net/ (specifically in net/url_request/redirect_util.cc).
+    //
+    // After both OOR-CORS and network service are fully shipped, we may be able
+    // to remove the logic in net/.
+    if ((fetch_cors_flag_ && NeedsPreflight(request_)) ||
+        (!original_fetch_cors_flag && fetch_cors_flag_) ||
+        (fetch_cors_flag_ && original_method != request_.method)) {
+      DCHECK_NE(request_.mode, mojom::RequestMode::kNoCors);
+      network_client_receiver_.reset();
+      sync_client_receiver_factory_.InvalidateWeakPtrs();
+      StartRequest();
+      return;
+    }
+
+    response_tainting_ = CalculateResponseTainting(
+        request_.url, request_.mode, request_.request_initiator,
+        request_.isolated_world_origin, fetch_cors_flag_, tainted_,
+        *origin_access_list_);
+    network_loader_->FollowRedirect(std::move(headers_update_params), new_url);
     return;
   }
 
+  if (fetch_cors_flag_ && request_.mode == mojom::RequestMode::kSameOrigin) {
+    CHECK(request_.request_initiator);
+    HandleComplete(URLLoaderCompletionStatus(
+        CorsErrorStatus(mojom::CorsError::kDisallowedByMode)));
+    return;
+  }
+
+  if (fetch_cors_flag_ && !skip_cors_enabled_scheme_check_ &&
+      !std::ranges::contains(url::GetCorsEnabledSchemes(),
+                             request_.url.scheme())) {
+    HandleComplete(URLLoaderCompletionStatus(
+        CorsErrorStatus(mojom::CorsError::kCorsDisabledScheme)));
+    return;
+  }
+
+  if (request_.mode != mojom::RequestMode::kNoCors) {
+    MaybeSetOriginHeader(&headers_update_params);
+  }
+
   response_tainting_ = CalculateResponseTainting(
       request_.url, request_.mode, request_.request_initiator,
       request_.isolated_world_origin, fetch_cors_flag_, tainted_,
       *origin_access_list_);
+
+  has_authorization_covered_by_wildcard_ = false;
+  if (CheckPreflightRequired()) {
+    CHECK(!deferred_redirect_preflight_.has_value());
+    deferred_redirect_preflight_ = DeferredRedirectPreflight{
+        .headers_update_params = std::move(headers_update_params),
+        .new_url = new_url,
+    };
+
+    StartPreflightCheck();
+    return;
+  }
+
   network_loader_->FollowRedirect(std::move(headers_update_params), new_url);
 }
 
@@ -921,7 +993,7 @@
 }
 
 std::optional<net::cookie_util::StorageAccessStatus>
-CorsURLLoader::GetStorageAccessStatus() {
+CorsURLLoader::GetStorageAccessStatus() const {
   if (isolation_info_.network_isolation_key().GetNonce()) {
     return net::cookie_util::StorageAccessStatus::kNone;
   }
@@ -971,48 +1043,75 @@
   return false;
 }
 
+bool CorsURLLoader::ShouldIncludeOriginHeader() const {
+  if (!request_.request_initiator) {
+    return false;
+  }
+
+  if (request_.credentials_mode == mojom::CredentialsMode::kInclude &&
+      GetStorageAccessStatus() ==
+          net::cookie_util::StorageAccessStatus::kInactive) {
+    // Lower layers will add the Sec-Fetch-Storage-Access header, and the
+    // server may respond with a "retry" header. The server needs to know the
+    // origin in that event.
+    return true;
+  }
+
+  // If the `CORS flag` is set, `httpRequest`’s method is neither `GET` nor
+  // `HEAD`, or `httpRequest`’s mode is "websocket", then append
+  // `Origin`/the result of serializing a request origin with `httpRequest`,
+  // to `httpRequest`’s header list.
+  //
+  // We exclude navigation requests to keep the existing behavior.
+  // TODO(yhirano): Reconsider this.
+  if (request_.mode == network::mojom::RequestMode::kNavigate) {
+    return false;
+  }
+  if (fetch_cors_flag_) {
+    return true;
+  }
+  return request_.method != net::HttpRequestHeaders::kGetMethod &&
+         request_.method != net::HttpRequestHeaders::kHeadMethod;
+}
+
+void CorsURLLoader::MaybeSetOriginHeader(
+    network::HttpRequestHeadersUpdateParams* headers_update_params) {
+  if (!ShouldIncludeOriginHeader()) {
+    return;
+  }
+
+  // If the Origin header is given, check if the initiator has a permission to
+  // override unsafe headers for the target URL. This Allowlist is given from
+  // a trustworthy process per factory, and safe to trust as a secondary
+  // security check here in the network service.
+  if (request_.headers.HasHeader(net::HttpRequestHeaders::kOrigin) &&
+      AllowUnsafeHeaders()) {
+    return;
+  }
+
+  std::string origin_value = tainted_
+                                 ? url::Origin().Serialize()
+                                 : request_.request_initiator->Serialize();
+  if (headers_update_params) {
+    std::erase(headers_update_params->removed_headers,
+               net::HttpRequestHeaders::kOrigin);
+    headers_update_params->modified_headers.SetHeader(
+        net::HttpRequestHeaders::kOrigin, origin_value);
+  }
+  request_.headers.SetHeader(net::HttpRequestHeaders::kOrigin, origin_value);
+}
+
 void CorsURLLoader::StartRequest() {
   TRACE_EVENT("loading", "CorsURLLoader::StartRequest",
               net::NetLogWithSourceToFlow(net_log_));
   if (fetch_cors_flag_ && !skip_cors_enabled_scheme_check_ &&
       !std::ranges::contains(url::GetCorsEnabledSchemes(),
-                             request_.url.GetScheme())) {
+                             request_.url.scheme())) {
     HandleComplete(URLLoaderCompletionStatus(
         CorsErrorStatus(mojom::CorsError::kCorsDisabledScheme)));
     return;
   }
 
-  auto should_include_origin_header = [&]() -> bool {
-    if (!request_.request_initiator) {
-      return false;
-    }
-
-    if (request_.credentials_mode == mojom::CredentialsMode::kInclude &&
-        GetStorageAccessStatus() ==
-            net::cookie_util::StorageAccessStatus::kInactive) {
-      // Lower layers will add the Sec-Fetch-Storage-Access header, and the
-      // server may respond with a "retry" header. The server needs to know the
-      // origin in that event.
-      return true;
-    }
-
-    // If the `CORS flag` is set, `httpRequest`’s method is neither `GET` nor
-    // `HEAD`, or `httpRequest`’s mode is "websocket", then append
-    // `Origin`/the result of serializing a request origin with `httpRequest`,
-    // to `httpRequest`’s header list.
-    //
-    // We exclude navigation requests to keep the existing behavior.
-    // TODO(yhirano): Reconsider this.
-    if (request_.mode == network::mojom::RequestMode::kNavigate) {
-      return false;
-    }
-    if (fetch_cors_flag_) {
-      return true;
-    }
-    return request_.method != net::HttpRequestHeaders::kGetMethod &&
-           request_.method != net::HttpRequestHeaders::kHeadMethod;
-  };
-
   std::optional<std::string> origin_header_value =
       request_.headers.GetHeader(net::HttpRequestHeaders::kOrigin);
   if (origin_header_value &&
@@ -1025,28 +1124,10 @@
     return;
   }
 
-  if (should_include_origin_header()) {
-    // If the Origin header is given, check if the initiator has a permission to
-    // override unsafe headers for the target URL. This Allowlist is given from
-    // a trustworthy process per factory, and safe to trust as a secondary
-    // security check here in the network service.
-    const bool has_custom_origin_header_with_bypass =
-        request_.headers.HasHeader(net::HttpRequestHeaders::kOrigin) &&
-        AllowUnsafeHeaders();
-
-    if (!has_custom_origin_header_with_bypass) {
-      if (tainted_) {
-        request_.headers.SetHeader(net::HttpRequestHeaders::kOrigin,
-                                   url::Origin().Serialize());
-      } else {
-        request_.headers.SetHeader(net::HttpRequestHeaders::kOrigin,
-                                   request_.request_initiator->Serialize());
-      }
-    }
-  }
+  MaybeSetOriginHeader(/*headers_update_params=*/nullptr);
 
   if (fetch_cors_flag_ && request_.mode == mojom::RequestMode::kSameOrigin) {
-    DCHECK(request_.request_initiator);
+    CHECK(request_.request_initiator);
     HandleComplete(URLLoaderCompletionStatus(
         CorsErrorStatus(mojom::CorsError::kDisallowedByMode)));
     return;
@@ -1056,19 +1137,8 @@
       request_.url, request_.mode, request_.request_initiator,
       request_.isolated_world_origin, fetch_cors_flag_, tainted_,
       *origin_access_list_);
-
-  // Note that even when `needs_preflight` holds we might not make a preflight
-  // request. This happens when `fetch_cors_flag_` is false, e.g. when the
-  // origin of the url is equal to the origin of the request.
-  std::optional<PreflightRequiredReason> needs_preflight =
-      NeedsPreflight(request_);
-  bool preflight_required = needs_preflight.has_value() && fetch_cors_flag_;
-  net_log_.AddEvent(net::NetLogEventType::CHECK_CORS_PREFLIGHT_REQUIRED, [&] {
-    return NetLogPreflightRequiredParams(needs_preflight);
-  });
-
   has_authorization_covered_by_wildcard_ = false;
-  if (!preflight_required) {
+  if (!CheckPreflightRequired()) {
     StartNetworkRequest();
     return;
   }
@@ -1077,6 +1147,19 @@
   // it now to free up the socket.
   network_loader_.reset();
 
+  StartPreflightCheck();
+}
+
+bool CorsURLLoader::CheckPreflightRequired() {
+  std::optional<PreflightRequiredReason> needs_preflight =
+      NeedsPreflight(request_);
+  net_log_.AddEvent(net::NetLogEventType::CHECK_CORS_PREFLIGHT_REQUIRED, [&] {
+    return NetLogPreflightRequiredParams(needs_preflight);
+  });
+  return needs_preflight.has_value() && fetch_cors_flag_;
+}
+
+void CorsURLLoader::StartPreflightCheck() {
   mojo::PendingRemote<mojom::URLLoaderNetworkServiceObserver> remote_observer;
 
   context_->cors_preflight_controller()->PerformPreflightCheck(
@@ -1150,6 +1233,9 @@
   has_authorization_covered_by_wildcard_ =
       has_authorization_covered_by_wildcard;
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/network/cors/cors_url_loader_unittest.cc b/services/network/cors/cors_url_loader_unittest.cc
index 67d24d14..4e7d3e1e 100644
--- a/services/network/cors/cors_url_loader_unittest.cc
+++ b/services/network/cors/cors_url_loader_unittest.cc
@@ -9,12 +9,14 @@
 #include <vector>
 
 #include "base/functional/callback_helpers.h"
+#include "base/run_loop.h"
 #include "base/strings/stringprintf.h"
+#include "base/test/run_until.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/test_future.h"
 #include "mojo/public/cpp/bindings/message.h"
 #include "mojo/public/cpp/system/functions.h"
-#include "net/base/features.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
 #include "net/base/load_flags.h"
 #include "net/cookies/cookie_util.h"
 #include "net/cookies/site_for_cookies.h"
@@ -55,6 +57,31 @@
 
 class CorsURLLoaderTest : public CorsURLLoaderTestBase {};
 
+class ParameterizedCorsURLLoaderTest
+    : public CorsURLLoaderTestBase,
+      public testing::WithParamInterface<bool> {
+ public:
+  void SetUp() override {
+    CorsURLLoaderTestBase::SetUp();
+    if (IsAvoidCorsURLLoaderRestartOnRedirectEnabled()) {
+      scoped_feature_list_.InitAndEnableFeature(
+          features::kAvoidCorsURLLoaderRestartOnRedirect);
+    } else {
+      scoped_feature_list_.InitAndDisableFeature(
+          features::kAvoidCorsURLLoaderRestartOnRedirect);
+    }
+  }
+
+  bool IsAvoidCorsURLLoaderRestartOnRedirectEnabled() const {
+    return GetParam();
+  }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+INSTANTIATE_TEST_SUITE_P(All, ParameterizedCorsURLLoaderTest, testing::Bool());
+
 class CorsURLLoaderTestWithSafeRevalidation
     : public CorsURLLoaderTestBase,
       public testing::WithParamInterface<bool> {
@@ -1250,7 +1277,7 @@
   EXPECT_EQ(net::OK, client().completion_status().error_code);
 }
 
-TEST_F(CorsURLLoaderTest, SameOriginToCrossOriginRedirect) {
+TEST_P(ParameterizedCorsURLLoaderTest, SameOriginToCrossOriginRedirect) {
   const GURL origin("https://example.com");
   const GURL url("https://example.com/foo.png");
   const GURL new_url("https://other.example.com/bar.png");
@@ -1273,11 +1300,13 @@
   ClearHasReceivedRedirect();
   FollowRedirect();
 
-  RunUntilCreateLoaderAndStartCalled();
-
-  // A new loader is created.
-  EXPECT_EQ(2, num_created_loaders());
-  EXPECT_EQ(GetRequest().url, new_url);
+  if (IsAvoidCorsURLLoaderRestartOnRedirectEnabled()) {
+    EXPECT_EQ(1, num_created_loaders());
+  } else {
+    RunUntilCreateLoaderAndStartCalled();
+    EXPECT_EQ(2, num_created_loaders());
+    EXPECT_EQ(GetRequest().url, new_url);
+  }
 
   NotifyLoaderClientOnReceiveResponse(
       {{"Access-Control-Allow-Origin", "https://example.com"}});
@@ -1288,6 +1317,15 @@
   EXPECT_TRUE(client().has_received_response());
   EXPECT_TRUE(client().has_received_completion());
   EXPECT_EQ(net::OK, client().completion_status().error_code);
+
+  if (IsAvoidCorsURLLoaderRestartOnRedirectEnabled()) {
+    ASSERT_TRUE(first_loader());
+    EXPECT_TRUE(first_loader()->follow_redirect_called());
+    EXPECT_EQ(
+        "https://example.com",
+        first_loader()->last_headers_update_params().modified_headers.GetHeader(
+            net::HttpRequestHeaders::kOrigin));
+  }
 }
 
 TEST_F(CorsURLLoaderTest, CrossOriginToCrossOriginRedirect) {
@@ -1418,7 +1456,7 @@
   EXPECT_EQ(net::OK, client().completion_status().error_code);
 }
 
-TEST_F(CorsURLLoaderTest,
+TEST_P(ParameterizedCorsURLLoaderTest,
        CrossOriginToAnotherCrossOriginRedirectWithPreflight) {
   const GURL origin("https://example.com");
   const GURL url("https://other.example.com/foo.png");
@@ -1471,16 +1509,24 @@
   NotifyLoaderClientOnReceiveResponse(
       {{"Access-Control-Allow-Origin", "null"},
        {"Access-Control-Allow-Methods", "PATCH"}});
-  RunUntilCreateLoaderAndStartCalled();
+  NotifyLoaderClientOnComplete(net::OK);
 
-  // the second actual request
-  EXPECT_EQ(4, num_created_loaders());
-  EXPECT_EQ(GetRequest().url, new_url);
-  EXPECT_EQ(GetRequest().method, "PATCH");
-  ASSERT_TRUE(GetRequest().request_initiator);
-  EXPECT_EQ(GetRequest().request_initiator->Serialize(), "https://example.com");
+  if (IsAvoidCorsURLLoaderRestartOnRedirectEnabled()) {
+    // When the second preflight completes, network_loader_->FollowRedirect() is
+    // called on the existing loader, so no fourth loader is created.
+    EXPECT_EQ(3, num_created_loaders());
+  } else {
+    RunUntilCreateLoaderAndStartCalled();
+    // the second actual request is created when the feature is disabled.
+    EXPECT_EQ(4, num_created_loaders());
+    EXPECT_EQ(GetRequest().url, new_url);
+    EXPECT_EQ(GetRequest().method, "PATCH");
+    ASSERT_TRUE(GetRequest().request_initiator);
+    EXPECT_EQ(GetRequest().request_initiator->Serialize(),
+              "https://example.com");
+  }
 
-  // The request is tainted, so the origin is "null".
+  // Notify response on the actual request and complete.
   NotifyLoaderClientOnReceiveResponse(
       {{"Access-Control-Allow-Origin", "null"}});
   NotifyLoaderClientOnComplete(net::OK);
@@ -1539,7 +1585,7 @@
   EXPECT_THAT(entry->params.FindString("failed-parameter"), IsNull());
 }
 
-TEST_F(CorsURLLoaderTest, RedirectInfoShouldBeUsed) {
+TEST_P(ParameterizedCorsURLLoaderTest, RedirectInfoShouldBeUsed) {
   const GURL origin("https://example.com");
   const GURL url("https://example.com/foo.png");
   const GURL new_url("https://other.example.com/foo.png");
@@ -1575,14 +1621,18 @@
 
   ClearHasReceivedRedirect();
   FollowRedirect();
-  RunUntilCreateLoaderAndStartCalled();
 
-  EXPECT_EQ(2, num_created_loaders());
-  EXPECT_EQ(new_url, GetRequest().url);
-  EXPECT_EQ("GET", GetRequest().method);
-  EXPECT_EQ(GURL("https://other.example.com"), GetRequest().referrer);
-  EXPECT_EQ(net::ReferrerPolicy::ORIGIN_ONLY_ON_TRANSITION_CROSS_ORIGIN,
-            GetRequest().referrer_policy);
+  if (IsAvoidCorsURLLoaderRestartOnRedirectEnabled()) {
+    EXPECT_EQ(1, num_created_loaders());
+  } else {
+    RunUntilCreateLoaderAndStartCalled();
+    EXPECT_EQ(2, num_created_loaders());
+    EXPECT_EQ(new_url, GetRequest().url);
+    EXPECT_EQ("GET", GetRequest().method);
+    EXPECT_EQ(GURL("https://other.example.com"), GetRequest().referrer);
+    EXPECT_EQ(net::ReferrerPolicy::ORIGIN_ONLY_ON_TRANSITION_CROSS_ORIGIN,
+              GetRequest().referrer_policy);
+  }
 
   NotifyLoaderClientOnReceiveResponse(
       {{"Access-Control-Allow-Origin", "https://example.com"}});
@@ -1599,7 +1649,7 @@
 // SiteForCookies values, the CorsURLLoader respects those changes. The former
 // only happens for frames, and the latter for subframes, but should make
 // assumptions about whether these need to be updated in CorsURLLoader.
-TEST_F(CorsURLLoaderTest,
+TEST_P(ParameterizedCorsURLLoaderTest,
        InterceptedRedirectChangesIsolationInfoAndSiteForCookies) {
   const GURL url("https://example.com/foo.png");
   const url::Origin url_origin = url::Origin::Create(url);
@@ -1645,18 +1695,22 @@
 
   ClearHasReceivedRedirect();
   FollowRedirect();
-  RunUntilCreateLoaderAndStartCalled();
 
-  EXPECT_EQ(2, num_created_loaders());
-  EXPECT_EQ(new_url, GetRequest().url);
-  EXPECT_EQ("GET", GetRequest().method);
-  EXPECT_TRUE(
-      GetRequest().site_for_cookies.IsEquivalent(new_url_site_for_cookies));
-  EXPECT_TRUE(GetRequest().trusted_params->isolation_info.IsEqualForTesting(
-      net::IsolationInfo::Create(net::IsolationInfo::RequestType::kMainFrame,
-                                 new_url_origin /* top_frame_origin */,
-                                 new_url_origin /* frame_origin */,
-                                 new_url_site_for_cookies)));
+  if (IsAvoidCorsURLLoaderRestartOnRedirectEnabled()) {
+    EXPECT_EQ(1, num_created_loaders());
+  } else {
+    RunUntilCreateLoaderAndStartCalled();
+    EXPECT_EQ(2, num_created_loaders());
+    EXPECT_EQ(new_url, GetRequest().url);
+    EXPECT_EQ("GET", GetRequest().method);
+    EXPECT_TRUE(
+        GetRequest().site_for_cookies.IsEquivalent(new_url_site_for_cookies));
+    EXPECT_TRUE(GetRequest().trusted_params->isolation_info.IsEqualForTesting(
+        net::IsolationInfo::Create(net::IsolationInfo::RequestType::kMainFrame,
+                                   new_url_origin /* top_frame_origin */,
+                                   new_url_origin /* frame_origin */,
+                                   new_url_site_for_cookies)));
+  }
 
   NotifyLoaderClientOnReceiveResponse(
       {{"Access-Control-Allow-Origin", "https://example.com"}});
@@ -1732,7 +1786,8 @@
   EXPECT_EQ(net::ERR_FAILED, client().completion_status().error_code);
 }
 
-TEST_F(CorsURLLoaderTest, CorsExemptHeaderRemovalOnCrossOriginRedirects) {
+TEST_P(ParameterizedCorsURLLoaderTest,
+       CorsExemptHeaderRemovalOnCrossOriginRedirects) {
   ResourceRequest request;
   request.url = GURL("https://example.com/foo.png");
   request.request_initiator = url::Origin::Create(GURL("https://example.com"));
@@ -1756,11 +1811,15 @@
   network::HttpRequestHeadersUpdateParams headers_update_params;
   headers_update_params.removed_headers.push_back(kTestCorsExemptHeader);
   FollowRedirect(std::move(headers_update_params));
-  RunUntilCreateLoaderAndStartCalled();
 
-  EXPECT_EQ(2, num_created_loaders());
-  EXPECT_FALSE(
-      GetRequest().cors_exempt_headers.HasHeader(kTestCorsExemptHeader));
+  if (IsAvoidCorsURLLoaderRestartOnRedirectEnabled()) {
+    EXPECT_EQ(1, num_created_loaders());
+  } else {
+    RunUntilCreateLoaderAndStartCalled();
+    EXPECT_EQ(2, num_created_loaders());
+    EXPECT_FALSE(
+        GetRequest().cors_exempt_headers.HasHeader(kTestCorsExemptHeader));
+  }
 }
 
 TEST_F(CorsURLLoaderTest, CorsExemptHeaderModificationOnRedirects) {
@@ -2649,9 +2708,7 @@
   NotifyLoaderClientOnReceiveResponse(
       {{"Access-Control-Allow-Origin", "null"},
        {"Access-Control-Allow-Headers", "x-custom"}});
-
-  RunUntilCreateLoaderAndStartCalled();
-
+  NotifyLoaderClientOnComplete(net::OK);
   if (IsTaintedOriginCacheKeyEnabled()) {
     // When kCorsPreflightCacheKeyTaintedOrigin is enabled, tainted preflights
     // are not cached, so no entry is added for new_url.
@@ -2674,6 +2731,127 @@
     // preflight is keyed under `initiator`. Verify that the preflight cache
     // contains the entry for other.example.org under expected_nik and NOT under
     // the stale NIK.
+    EXPECT_TRUE(base::test::RunUntil([&]() {
+      return network_context()
+          ->cors_preflight_controller()
+          ->GetPreflightCacheForTesting()
+          .DoesEntryExistForTesting(initiator, new_url.spec(), expected_nik);
+    }));
+    EXPECT_FALSE(
+        network_context()
+            ->cors_preflight_controller()
+            ->GetPreflightCacheForTesting()
+            .DoesEntryExistForTesting(initiator, new_url.spec(), stale_nik));
+    EXPECT_EQ(2u, network_context()
+                      ->cors_preflight_controller()
+                      ->GetPreflightCacheForTesting()
+                      .CountEntriesForTesting());
+  }
+
+  // When the second preflight completes, network_loader_->FollowRedirect() is
+  // called on the existing loader, so no fourth loader is created.
+  EXPECT_EQ(3, num_created_loaders());
+
+  NotifyLoaderClientOnReceiveResponse(
+      {{"Access-Control-Allow-Origin", "null"}});
+  NotifyLoaderClientOnComplete(net::OK);
+  RunUntilComplete();
+
+  EXPECT_TRUE(client().has_received_response());
+  EXPECT_TRUE(client().has_received_completion());
+  EXPECT_EQ(net::OK, client().completion_status().error_code);
+}
+
+TEST_P(CorsURLLoaderTestWithTaintedOriginCacheKey,
+       RestrictedPrefetchRedirectUpdatesIsolationInfo_FeatureDisabled) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitAndDisableFeature(
+      features::kAvoidCorsURLLoaderRestartOnRedirect);
+
+  url::Origin initiator = url::Origin::Create(GURL("https://example.com"));
+  const GURL url("https://other.example.com/foo.png");
... (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.