Chrome · Extensions
CVE-2026-87537
Logic Error in Extensions
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
TestRedirectInterceptorcontent/browser/loader/navigation_url_loader_impl_unittest.cc |
modified | |
frame_tree_node_id_ptr_content/browser/loader/navigation_url_loader_impl_unittest.cc |
modified |
Files Changed
content/browser/blob_storage/blob_url_browsertest.cccontent/browser/loader/navigation_url_loader_impl.cccontent/browser/loader/navigation_url_loader_impl_unittest.cc
Patch
From 40aa5a35e86fe4c46f19655c29984e93a1b20b5f Mon Sep 17 00:00:00 2001 From: Nidhi Jaju <[email protected]> Date: Thu, 13 Aug 2026 20:48:55 -0700 Subject: [PATCH] [Navigation] Move redirect check bypass state to NavigationHandle Currently, bypass_redirect_checks is passed across process boundaries in network::mojom::URLResponseHead. However, this flag is only ever intended to be set by browser-process proxying URLLoaderFactories (such as the WebRequest API proxying loader factory on the UI thread) when authorizing a redirect. This CL removes bypass_redirect_checks from URLResponseHead and instead records the authorization state directly on the UI-thread NavigationHandle/NavigationRequest identified by the navigation_id. When NavigationURLLoaderImpl receives a redirect, it consumes the in-memory flag from NavigationHandle rather than reading from URLResponseHead. Bug: 498732709 Change-Id: I62d17b56bac8a94f7be03772620149b9ab9ba9af Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8254718 Commit-Queue: Nidhi Jaju <[email protected]> Reviewed-by: Rakina Zata Amni <[email protected]> Reviewed-by: Sam McNally <[email protected]> Reviewed-by: Andrea Orru <[email protected]> Cr-Commit-Position: refs/heads/main@{#1679332} --- diff --git a/content/browser/blob_storage/blob_url_browsertest.cc b/content/browser/blob_storage/blob_url_browsertest.cc index 08c747b..5010588b 100644 --- a/content/browser/blob_storage/blob_url_browsertest.cc +++ b/content/browser/blob_storage/blob_url_browsertest.cc @@ -293,7 +293,6 @@ head->headers = net::HttpResponseHeaders::TryToCreate( "HTTP/1.1 302 Found\r\nLocation: " + redirect_target_.spec() + "\r\n"); head->encoded_data_length = 0; - head->bypass_redirect_checks = true; client_->OnReceiveRedirect(redirect_info, std::move(head)); } void ReadSideData(ReadSideDataCallback) override { NOTREACHED(); } diff --git a/content/browser/loader/navigation_url_loader_impl.cc b/content/browser/loader/navigation_url_loader_impl.cc index 6c52418..bf56800 100644 --- a/content/browser/loader/navigation_url_loader_impl.cc +++ b/content/browser/loader/navigation_url_loader_impl.cc @@ -66,6 +66,7 @@ #include "content/public/browser/content_browser_client.h" #include "content/public/browser/download_utils.h" #include "content/public/browser/frame_accept_header.h" +#include "content/public/browser/navigation_handle.h" #include "content/public/browser/navigation_ui_data.h" #include "content/public/browser/network_service_instance.h" #include "content/public/browser/network_service_util.h" @@ -1567,10 +1568,27 @@ resource_request().is_outermost_main_frame); net::Error error = net::OK; - bool bypass_redirect_checks = - base::FeatureList::IsEnabled(features::kBypassRedirectChecksPerRequest) - ? head->bypass_redirect_checks - : bypass_redirect_checks_; + bool bypass_redirect_checks = false; + if (base::FeatureList::IsEnabled(features::kBypassRedirectChecksPerRequest)) { + // A proxying URLLoaderFactory may authorize a redirect to bypass safety + // checks. This authorization is set directly on the NavigationRequest + // in the browser process. NavigationURLLoaderImpl doesn't have a direct + // pointer to the NavigationRequest, so we look it up via the + // FrameTreeNode. We check the navigation ID to ensure we don't apply the + // bypass to a different navigation in the same frame. + if (FrameTreeNode* frame_tree_node = + FrameTreeNode::GloballyFindByID(frame_tree_node_id_)) { + if (NavigationRequest* nav_request = + frame_tree_node->navigation_request()) { + if (nav_request->GetNavigationId() == request_info_->navigation_id) { + bypass_redirect_checks = + nav_request->ConsumeBypassRedirectChecksForNextRedirect(); + } + } + } + } else { + bypass_redirect_checks = bypass_redirect_checks_; + } if (url_.SchemeIsBlob()) { // Loading a blob URL never produces a redirect. diff --git a/content/browser/loader/navigation_url_loader_impl_unittest.cc b/content/browser/loader/navigation_url_loader_impl_unittest.cc index d756567..df925305 100644 --- a/content/browser/loader/navigation_url_loader_impl_unittest.cc +++ b/content/browser/loader/navigation_url_loader_impl_unittest.cc @@ -30,6 +30,7 @@ #include "content/public/browser/navigation_ui_data.h" #include "content/public/browser/storage_partition.h" #include "content/public/common/buildflags.h" +#include "content/public/common/content_features.h" #include "content/public/common/content_switches.h" #include "content/public/test/browser_task_environment.h" #include "content/public/test/mock_client_hints_controller_delegate.h" @@ -38,6 +39,7 @@ #include "content/public/test/test_renderer_host.h" #include "content/test/test_navigation_url_loader_delegate.h" #include "content/test/test_web_contents.h" +#include "mojo/public/cpp/bindings/remote.h" #include "net/base/load_flags.h" #include "net/base/mock_network_change_notifier.h" #include "net/proxy_resolution/configured_proxy_resolution_service.h" @@ -200,10 +202,12 @@ nullptr /* blob_url_loader_factory */, base::UnguessableToken::Create() /* devtools_navigation_token */, base::UnguessableToken::Create() /* devtools_frame_token */, - nullptr /* client_security_state */, - false /* is_pdf */, ChildProcessId() /* initiator_process_id */, + nullptr /* client_security_state */, false /* is_pdf */, + ChildProcessId() /* initiator_process_id */, std::nullopt /* initiator_document_token */, - false /* allow_cookies_from_browser */, 0 /* navigation_id */, + false /* allow_cookies_from_browser */, + pending_navigation_->GetNavigationHandle() + ->GetNavigationId() /* navigation_id */, is_ad_tagged /* is_ad_tagged */, false /* force_no_https_upgrade */)); @@ -698,6 +702,78 @@ bool should_redirect_ = true; }; +// A `NavigationLoaderInterceptor` that intercepts the request via +// `MaybeCreateLoader()` and immediately issues a redirect to `redirect_url` +// with `URLResponseHead::bypass_redirect_checks` set to the supplied value. +class TestRedirectInterceptor final : public NavigationLoaderInterceptor { + public: + TestRedirectInterceptor(const GURL& redirect_url, + bool bypass_redirect_checks, + int64_t* navigation_id_ptr = nullptr, + FrameTreeNodeId* frame_tree_node_id = nullptr) + : redirect_url_(redirect_url), + bypass_redirect_checks_(bypass_redirect_checks), + navigation_id_ptr_(navigation_id_ptr), + frame_tree_node_id_ptr_(frame_tree_node_id) {} + ~TestRedirectInterceptor() override = default; + + private: + void MaybeCreateLoader( + const network::ResourceRequest& tentative_resource_request, + BrowserContext* browser_context, + LoaderCallback callback, + FallbackCallback fallback_callback) override { + auto factory = base::MakeRefCounted<network::SingleRequestURLLoaderFactory>( + base::BindOnce(&TestRedirectInterceptor::HandleRequest, + base::Unretained(this))); + std::move(callback).Run(NavigationLoaderInterceptor::Result( + std::move(factory), SubresourceLoaderParams())); + } + + bool MaybeCreateLoaderForResponse( + const network::URLLoaderCompletionStatus& status, + const network::ResourceRequest& request, + network::mojom::URLResponseHeadPtr* response_head, + mojo::ScopedDataPipeConsumerHandle* response_body, + mojo::PendingReceiver<network::mojom::URLLoaderClient>* client_receiver, + blink::ThrottlingURLLoader* url_loader, + bool* skip_other_interceptors) override { + return false; + } + + void HandleRequest( + const network::ResourceRequest& request, + mojo::PendingReceiver<network::mojom::URLLoader> loader, + mojo::PendingRemote<network::mojom::URLLoaderClient> client) { + loader_receiver_ = std::move(loader); + client_.Bind(std::move(client)); + + auto head = network::mojom::URLResponseHead::New(); + if (bypass_redirect_checks_ && navigation_id_ptr_ && + *navigation_id_ptr_ != 0 && frame_tree_node_id_ptr_) { + NavigationHandle::SetBypassRedirectChecksForNextRedirect( + *frame_tree_node_id_ptr_, *navigation_id_ptr_); + } + net::RedirectInfo redirect_info = net::RedirectInfo::ComputeRedirectInfo( + request.method, request.url, request.site_for_cookies, + request.update_first_party_url_on_redirect + ? net::RedirectInfo::FirstPartyURLPolicy::UPDATE_URL_ON_REDIRECT + : net::RedirectInfo::FirstPartyURLPolicy::NEVER_CHANGE_URL, + request.referrer_policy, request.referrer.spec(), + request.request_initiator, net::HTTP_TEMPORARY_REDIRECT, redirect_url_, + /*referrer_policy_header=*/std::nullopt, + /*insecure_scheme_was_upgraded=*/false); + client_->OnReceiveRedirect(redirect_info, std::move(head)); + } + + const GURL redirect_url_; + const bool bypass_redirect_checks_; + const raw_ptr<int64_t> navigation_id_ptr_; + const raw_ptr<FrameTreeNodeId> frame_tree_node_id_ptr_; + mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver_; + mojo::Remote<network::mojom::URLLoaderClient> client_; +}; + // This sets the timeout timer but doesn't expect the timer is fired // automatically. If needed, the timer should be fired explicitly e.g. via // `TriggerTimeoutForTesting()`. @@ -1370,6 +1446,68 @@
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/content/browser/blob_storage/blob_url_browsertest.cc b/content/browser/blob_storage/blob_url_browsertest.cc
index 08c747b..5010588b 100644
--- a/content/browser/blob_storage/blob_url_browsertest.cc
+++ b/content/browser/blob_storage/blob_url_browsertest.cc
@@ -293,7 +293,6 @@
head->headers = net::HttpResponseHeaders::TryToCreate(
"HTTP/1.1 302 Found\r\nLocation: " + redirect_target_.spec() + "\r\n");
head->encoded_data_length = 0;
- head->bypass_redirect_checks = true;
client_->OnReceiveRedirect(redirect_info, std::move(head));
}
void ReadSideData(ReadSideDataCallback) override { NOTREACHED(); }
diff --git a/content/browser/loader/navigation_url_loader_impl_unittest.cc b/content/browser/loader/navigation_url_loader_impl_unittest.cc
index d756567..df925305 100644
--- a/content/browser/loader/navigation_url_loader_impl_unittest.cc
+++ b/content/browser/loader/navigation_url_loader_impl_unittest.cc
@@ -30,6 +30,7 @@
#include "content/public/browser/navigation_ui_data.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/buildflags.h"
+#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/mock_client_hints_controller_delegate.h"
@@ -38,6 +39,7 @@
#include "content/public/test/test_renderer_host.h"
#include "content/test/test_navigation_url_loader_delegate.h"
#include "content/test/test_web_contents.h"
+#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/load_flags.h"
#include "net/base/mock_network_change_notifier.h"
#include "net/proxy_resolution/configured_proxy_resolution_service.h"
@@ -200,10 +202,12 @@
nullptr /* blob_url_loader_factory */,
base::UnguessableToken::Create() /* devtools_navigation_token */,
base::UnguessableToken::Create() /* devtools_frame_token */,
- nullptr /* client_security_state */,
- false /* is_pdf */, ChildProcessId() /* initiator_process_id */,
+ nullptr /* client_security_state */, false /* is_pdf */,
+ ChildProcessId() /* initiator_process_id */,
std::nullopt /* initiator_document_token */,
- false /* allow_cookies_from_browser */, 0 /* navigation_id */,
+ false /* allow_cookies_from_browser */,
+ pending_navigation_->GetNavigationHandle()
+ ->GetNavigationId() /* navigation_id */,
is_ad_tagged /* is_ad_tagged */,
false /* force_no_https_upgrade */));
@@ -698,6 +702,78 @@
bool should_redirect_ = true;
};
+// A `NavigationLoaderInterceptor` that intercepts the request via
+// `MaybeCreateLoader()` and immediately issues a redirect to `redirect_url`
+// with `URLResponseHead::bypass_redirect_checks` set to the supplied value.
+class TestRedirectInterceptor final : public NavigationLoaderInterceptor {
+ public:
+ TestRedirectInterceptor(const GURL& redirect_url,
+ bool bypass_redirect_checks,
+ int64_t* navigation_id_ptr = nullptr,
+ FrameTreeNodeId* frame_tree_node_id = nullptr)
+ : redirect_url_(redirect_url),
+ bypass_redirect_checks_(bypass_redirect_checks),
+ navigation_id_ptr_(navigation_id_ptr),
+ frame_tree_node_id_ptr_(frame_tree_node_id) {}
+ ~TestRedirectInterceptor() override = default;
+
+ private:
+ void MaybeCreateLoader(
+ const network::ResourceRequest& tentative_resource_request,
+ BrowserContext* browser_context,
+ LoaderCallback callback,
+ FallbackCallback fallback_callback) override {
+ auto factory = base::MakeRefCounted<network::SingleRequestURLLoaderFactory>(
+ base::BindOnce(&TestRedirectInterceptor::HandleRequest,
+ base::Unretained(this)));
+ std::move(callback).Run(NavigationLoaderInterceptor::Result(
+ std::move(factory), SubresourceLoaderParams()));
+ }
+
+ bool MaybeCreateLoaderForResponse(
+ const network::URLLoaderCompletionStatus& status,
+ const network::ResourceRequest& request,
+ network::mojom::URLResponseHeadPtr* response_head,
+ mojo::ScopedDataPipeConsumerHandle* response_body,
+ mojo::PendingReceiver<network::mojom::URLLoaderClient>* client_receiver,
+ blink::ThrottlingURLLoader* url_loader,
+ bool* skip_other_interceptors) override {
+ return false;
+ }
+
+ void HandleRequest(
+ const network::ResourceRequest& request,
+ mojo::PendingReceiver<network::mojom::URLLoader> loader,
+ mojo::PendingRemote<network::mojom::URLLoaderClient> client) {
+ loader_receiver_ = std::move(loader);
+ client_.Bind(std::move(client));
+
+ auto head = network::mojom::URLResponseHead::New();
+ if (bypass_redirect_checks_ && navigation_id_ptr_ &&
+ *navigation_id_ptr_ != 0 && frame_tree_node_id_ptr_) {
+ NavigationHandle::SetBypassRedirectChecksForNextRedirect(
+ *frame_tree_node_id_ptr_, *navigation_id_ptr_);
+ }
+ net::RedirectInfo redirect_info = net::RedirectInfo::ComputeRedirectInfo(
+ request.method, request.url, request.site_for_cookies,
+ request.update_first_party_url_on_redirect
+ ? net::RedirectInfo::FirstPartyURLPolicy::UPDATE_URL_ON_REDIRECT
+ : net::RedirectInfo::FirstPartyURLPolicy::NEVER_CHANGE_URL,
+ request.referrer_policy, request.referrer.spec(),
+ request.request_initiator, net::HTTP_TEMPORARY_REDIRECT, redirect_url_,
+ /*referrer_policy_header=*/std::nullopt,
+ /*insecure_scheme_was_upgraded=*/false);
+ client_->OnReceiveRedirect(redirect_info, std::move(head));
+ }
+
+ const GURL redirect_url_;
+ const bool bypass_redirect_checks_;
+ const raw_ptr<int64_t> navigation_id_ptr_;
+ const raw_ptr<FrameTreeNodeId> frame_tree_node_id_ptr_;
+ mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver_;
+ mojo::Remote<network::mojom::URLLoaderClient> client_;
+};
+
// This sets the timeout timer but doesn't expect the timer is fired
// automatically. If needed, the timer should be fired explicitly e.g. via
// `TriggerTimeoutForTesting()`.
@@ -1370,6 +1446,68 @@
Optional(std::string("Value3")));
}
+// `URLResponseHead::bypass_redirect_checks` is delivered over the
+// `URLLoaderClient` pipe and must not by itself allow a redirect to a target
+// that fails `IsSafeRedirectTarget()`. The per-request bit is only honored when
+// the loader factory in use was created with `bypass_redirect_checks` set
+// (i.e., a browser-process proxy is responsible for the redirect).
+TEST_F(NavigationURLLoaderImplTest,
+ PerRequestBypassRedirectChecksRequiresFactoryFlag) {
+ base::test::ScopedFeatureList feature_list{
+ features::kBypassRedirectChecksPerRequest};
+ ASSERT_TRUE(http_test_server_.Start());
+
+ for (bool bypass : {true, false}) {
+ SCOPED_TRACE(testing::Message() << "bypass_redirect_checks=" << bypass);
+ TestNavigationURLLoaderDelegate delegate;
+ std::vector<std::unique_ptr<NavigationLoaderInterceptor>> interceptors;
+ interceptors.push_back(std::make_unique<TestRedirectInterceptor>(
+ GURL("file:///"), /*bypass_redirect_checks=*/bypass));
+ auto loader =
+ CreateTestLoader(http_test_server_.GetURL("/echo"), std::string(),
+ "GET", &delegate, blink::NavigationDownloadPolicy(),
+ /*is_main_frame=*/true,
+ /*upgrade_if_insecure=*/false,
+ /*is_ad_tagged=*/false, std::move(interceptors));
+ loader->Start();
+ delegate.WaitForRequestFailed();
+ EXPECT_EQ(delegate.on_redirect_handled_counter(), 0);
+ EXPECT_EQ(delegate.on_request_handled_counter(), 1);
+ EXPECT_EQ(net::ERR_UNSAFE_REDIRECT, delegate.net_error());
+ }
+}
+
+// When a browser-process proxy authorizes bypassing redirect checks via
+// authorizes bypassing redirect checks via
+// `NavigationHandle::SetBypassRedirectChecksForNextRedirect()`, the redirect is
+// allowed even if `IsSafeRedirectTarget()` would normally fail.
+TEST_F(NavigationURLLoaderImplTest,
+ PerRequestBypassRedirectChecksSucceedsWhenAuthorizedByProxy) {
+ base::test::ScopedFeatureList feature_list{
+ features::kBypassRedirectChecksPerRequest};
+ ASSERT_TRUE(http_test_server_.Start());
+
+ int64_t navigation_id = 0;
+ FrameTreeNodeId frame_tree_node_id;
+ TestNavigationURLLoaderDelegate delegate;
+ std::vector<std::unique_ptr<NavigationLoaderInterceptor>> interceptors;
+ interceptors.push_back(std::make_unique<TestRedirectInterceptor>(
+ GURL("file:///"), /*bypass_redirect_checks=*/true, &navigation_id,
+ &frame_tree_node_id));
+ auto loader =
+ CreateTestLoader(http_test_server_.GetURL("/echo"), std::string(), "GET",
+ &delegate, blink::NavigationDownloadPolicy(),
+ /*is_main_frame=*/true,
+ /*upgrade_if_insecure=*/false,
+ /*is_ad_tagged=*/false, std::move(interceptors));
+ navigation_id = pending_navigation_->GetNavigationHandle()->GetNavigationId();
+ frame_tree_node_id =
+ pending_navigation_->GetNavigationHandle()->GetFrameTreeNodeId();
+ loader->Start();
+ delegate.WaitForRequestRedirected();
+ EXPECT_EQ(delegate.on_redirect_handled_counter(), 1);
+}
+
// Tests that the Upgrade If Insecure flag is obeyed.
TEST_F(NavigationURLLoaderImplTest, UpgradeIfInsecureTest) {
ASSERT_TRUE(http_test_server_.Start());
diff --git a/content/public/test/mock_navigation_handle.h b/content/public/test/mock_navigation_handle.h
index 155acda..3389103 100644
--- a/content/public/test/mock_navigation_handle.h
+++ b/content/public/test/mock_navigation_handle.h
@@ -280,6 +280,8 @@
());
MOCK_METHOD(bool, NeedsUrlLoader, ());
MOCK_METHOD(bool, IsInitialWebUINavigation, ());
+ MOCK_METHOD(void, SetBypassRedirectChecksForNextRedirect, (bool));
+ MOCK_METHOD(bool, ConsumeBypassRedirectChecksForNextRedirect, ());
#if BUILDFLAG(IS_ANDROID)
MOCK_METHOD(const base::android::JavaRef<jobject>&,
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.
References
On This Page