Chrome · API › Extensions
CVE-2026-113094
Memory Corruption in API (Extensions)
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifextensions/browser/api/web_request/web_request_proxying_url_loader_factory.cc |
modified | |
RejectFollowRedirectextensions/browser/api/web_request/web_request_proxying_url_loader_factory.cc |
modified | |
WebRequestProxyingURLLoaderFactoryFollowRedirectTestchrome/browser/extensions/api/web_request/web_request_proxying_url_loader_factory_browsertest.cc |
modified |
Files Changed
extensions/browser/api/web_request/web_request_proxying_url_loader_factory.ccextensions/browser/api/web_request/web_request_proxying_url_loader_factory.hchrome/browser/extensions/api/web_request/web_request_proxying_url_loader_factory_browsertest.cctools/metrics/histograms/enums.xmlchrome/browser/extensions/BUILD.gn
Patch
From ecf2b4392a7a7d7b3b93d310b09996c963093a3a Mon Sep 17 00:00:00 2001
From: Devlin Cronin <rdevlin.cronin@chromium.org>
Date: Mon, 21 Sep 2026 18:47:12 -0700
Subject: [PATCH] [Extensions] Reject unexpected FollowRedirect in WebRequest proxy loader
When a WebRequest listener blocks asynchronously during onBeforeRequest
(returning net::ERR_IO_PENDING), the proxied URLLoader receiver remains
open. A compromised renderer could send an unsolicited FollowRedirect()
call to mutate `request_.url` and trigger RestartInternal(). Because
`kOnBeforeRequest` had already been marked as signaled for the request,
the restarted request skipped extension listener evaluation and
dispatched directly to the network, bypassing extension blocking rules.
This CL fixes the vulnerability by tracking `deferred_redirect_url_` in
WebRequestProxyingURLLoaderFactory::InProgressRequest, set only when a
redirect is legitimately forwarded to the client. Violations report a
bad message and terminate the renderer.
Also add a regression test.
Bug: 497494634
Change-Id: I43b3cd43415a8f78366a68c9cd8d531bf63384db
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8423651
Reviewed-by: Kelvin Jiang <kelvinjiang@chromium.org>
Commit-Queue: Devlin Cronin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1702297}
---
diff --git a/extensions/browser/api/web_request/web_request_proxying_url_loader_factory.cc b/extensions/browser/api/web_request/web_request_proxying_url_loader_factory.cc
index b3a850e..70f76efb 100644
--- a/extensions/browser/api/web_request/web_request_proxying_url_loader_factory.cc
+++ b/extensions/browser/api/web_request/web_request_proxying_url_loader_factory.cc
@@ -378,6 +378,34 @@
void WebRequestProxyingURLLoaderFactory::InProgressRequest::FollowRedirect(
network::HttpRequestHeadersUpdateParams headers_update_params,
const std::optional<GURL>& new_url) {
+ // Only follow a redirect this class actually forwarded to the client. A
+ // compromised renderer could send an unexpected "FollowRedirect()", which
+ // would otherwise swap `request_.url` for the new URL.
+ // See crbug.com/497494634.
+ //
+ // NOTE: RejectFollowRedirect() deletes `this`, so every call site below must
+ // return immediately without touching any member state.
+ if (!deferred_redirect_url_) {
+ RejectFollowRedirect("Unexpected FollowRedirect");
+ return;
+ }
+
+ // `new_url` lets throttles adjust the redirect destination, but per the
+ // network::mojom::URLLoader contract it must be same-origin, same-scheme and
+ // credential-free. Kept in sync with CorsURLLoader::FollowRedirect().
+ if (new_url && (!new_url->is_valid() ||
+ new_url->scheme() != deferred_redirect_url_->scheme() ||
+ !url::IsSameOriginWith(*new_url, *deferred_redirect_url_))) {
+ RejectFollowRedirect("Unexpected new_url in FollowRedirect");
+ return;
+ }
+ if (new_url && (new_url->has_username() || new_url->has_password())) {
+ RejectFollowRedirect("new_url with credentials in FollowRedirect");
+ return;
+ }
+
+ deferred_redirect_url_.reset();
+
if (new_url) {
request_.url = new_url.value();
}
@@ -435,6 +463,8 @@
perfetto::Flow::ProcessScoped(profile_request_id_,
kWebRequestProxyingURLLoaderFactoryScope));
+ // A response can only arrive once any forwarded redirect has been followed.
+ CHECK(!deferred_redirect_url_);
current_body_ = std::move(body);
current_cached_metadata_ = std::move(cached_metadata);
if (current_request_uses_header_client_) {
@@ -543,6 +573,8 @@
return;
}
+ // As in OnReceiveResponse(); success implies the redirect was followed.
+ CHECK(!deferred_redirect_url_);
state_ = kCompleted;
target_client_->OnComplete(status);
WebRequestEventRouter::Get(factory_->browser_context_)
@@ -1303,6 +1335,7 @@
WebRequestEventRouter::Get(factory_->browser_context_)
->OnBeforeRedirect(factory_->browser_context_, &info_.value(),
redirect_info.new_url);
+ deferred_redirect_url_ = redirect_info.new_url;
target_client_->OnReceiveRedirect(redirect_info, current_response_.Clone());
request_.UpdateOnRedirect(redirect_info);
@@ -1397,6 +1430,22 @@
factory_->RemoveRequest(request_id_for_network_service_, profile_request_id_);
}
+void WebRequestProxyingURLLoaderFactory::InProgressRequest::
+ RejectFollowRedirect(std::string_view reason, net::Error error_code) {
+ // This proxy is also used for navigations, service worker script loads and
+ // browser-initiated prefetches, whose client lives in the browser process.
+ // Reporting a bad message there would take down the browser for what is a
+ // browser-side bug, so only fail the request. This matches similar behavior
+ // in CorsURLLoader.
+ if (factory_->render_process_id_ != -1) {
+ proxied_loader_receiver_.ReportBadMessage(reason);
+ }
+
+ // Deletes `this`.
+ OnRequestError(CreateURLLoaderCompletionStatus(error_code),
+ State::kRejectedByUnexpectedFollowRedirect);
+}
+
void WebRequestProxyingURLLoaderFactory::InProgressRequest::OnNetworkError(
const network::URLLoaderCompletionStatus& status) {
TRACE_EVENT(
diff --git a/extensions/browser/api/web_request/web_request_proxying_url_loader_factory.h b/extensions/browser/api/web_request/web_request_proxying_url_loader_factory.h
index 91b67a6..d1a77ad 100644
--- a/extensions/browser/api/web_request/web_request_proxying_url_loader_factory.h
+++ b/extensions/browser/api/web_request/web_request_proxying_url_loader_factory.h
@@ -11,6 +11,7 @@
#include <optional>
#include <set>
#include <string>
+#include <string_view>
#include <vector>
#include "base/callback_list.h"
@@ -168,7 +169,9 @@
kRejectedByOnHeadersReceivedForAuth,
kRejectedByOnAuthRequired,
kCompleted,
- kMaxValue = kCompleted,
+ // The client's FollowRedirect() violated the URLLoader contract.
+ kRejectedByUnexpectedFollowRedirect,
+ kMaxValue = kRejectedByUnexpectedFollowRedirect,
};
// These two methods combined form the implementation of Restart().
void UpdateRequestInfo();
@@ -202,6 +205,14 @@
net::CompletionOnceCallback continuation);
void OnRequestError(const network::URLLoaderCompletionStatus& status,
State state);
+
+ // Terminates a request whose client violated the URLLoader contract for
+ // FollowRedirect(), and reports `reason` as a bad message if the client is
+ // a renderer process. Deletes `this`.
+ void RejectFollowRedirect(
+ std::string_view reason,
+ net::Error error_code = net::ERR_INVALID_ARGUMENT);
+
void OnNetworkError(const network::URLLoaderCompletionStatus& status);
void OnClientDisconnected();
void OnLoaderDisconnected(uint32_t custom_reason,
@@ -252,8 +263,17 @@
mojo::ScopedDataPipeConsumerHandle current_body_;
std::optional<mojo_base::BigBuffer> current_cached_metadata_;
scoped_refptr<net::HttpResponseHeaders> override_headers_;
+
+ // A redirect *requested by an extension*, which this class then
+ // synthesizes. Empty if no extension asked to redirect the current hop.
+ // Not to be confused with `deferred_redirect_url_` below.
GURL redirect_url_;
+ // The target of a redirect already forwarded to the `target_client_` via
+ // OnReceiveRedirect(), but not yet followed. Must be set for a legitimate
+ // call to `FollowRedirect()`.
+ std::optional<GURL> deferred_redirect_url_;
+
// Holds any provided auth credentials through the extent of the request's
// lifetime.
std::optional<net::AuthCredentials> auth_credentials_;
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 6585c24..143d1cf45 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -3776,6 +3776,8 @@
<int value="12" label="Rejected by onHeadersReceived for HTTP 401"/>
<int value="13" label="Rejected by onAuthRequired()"/>
<int value="14" label="The request is completed"/>
+ <int value="15"
+ label="Rejected because the client sent an invalid FollowRedirect"/>
</enum>
<enum name="ExtensionInstallationFailureReason">
diff --git a/chrome/browser/extensions/api/web_request/web_request_proxying_url_loader_factory_browsertest.cc b/chrome/browser/extensions/api/web_request/web_request_proxying_url_loader_factory_browsertest.cc
new file mode 100644
index 0000000..8635585
--- /dev/null
+++ b/chrome/browser/extensions/api/web_request/web_request_proxying_url_loader_factory_browsertest.cc
@@ -0,0 +1,364 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <optional>
+#include <set>
+#include <string>
+
+#include "base/command_line.h"
+#include "base/strings/stringprintf.h"
+#include "base/synchronization/lock.h"
+#include "chrome/browser/extensions/extension_apitest.h"
+#include "chrome/browser/profiles/profile.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "extensions/browser/api/web_request/web_request_api.h"
+#include "extensions/browser/browser_context_keyed_api_factory.h"
+#include "extensions/common/extension.h"
+#include "extensions/common/switches.h"
+#include "extensions/test/extension_test_message_listener.h"
+#include "extensions/test/test_extension_dir.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/url_loader.mojom.h"
+#include "services/network/public/mojom/url_loader_factory.mojom.h"
+#include "services/network/test/test_url_loader_client.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace extensions {
+
+namespace {
+
+// Key and ID for an allowlisted MV3 extension permitted to use the
+// "webRequestBlocking" permission.
+constexpr char kAllowlistedExtensionKey[] =
+ "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8xv6iO+j4kzj1HiBL93+XVJH/CRyAQMUHS/"
+ "Z0l8nCAzaAFkW/JsNwxJqQhrZspnxLqbQxNncXs6g6bsXAwKHiEs+LSs+bIv0Gc/2ycZdhXJ8G"
+ "hEsSMakog5dpQd1681c2gLK/8CrAoewE/0GIKhaFcp7a2iZlGh4Am6fgMKy0iQIDAQAB";
+constexpr char kAllowlistedExtensionId[] = "ddchlicdkolnonkihahngkmmmjnjlkkf";
+
+} // namespace
+
+// Test fixture for validation of URLLoader::FollowRedirect() in
+// WebRequestProxyingURLLoaderFactory. Regression coverage for
+// crbug.com/497494634, where a compromised renderer could send an unsolicited
+// FollowRedirect() to swap the request URL out from under a pending blocking
+// webRequest.onBeforeRequest listener, reaching a URL the listener never saw.
+//
+// These tests drive the frame's URLLoaderFactory directly, which is what a
+// compromised renderer has access to.
+class WebRequestProxyingURLLoaderFactoryFollowRedirectTest
+ : public ExtensionApiTest {
+ public:
+ WebRequestProxyingURLLoaderFactoryFollowRedirectTest() = default;
+ ~WebRequestProxyingURLLoaderFactoryFollowRedirectTest() override = default;
+
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ ExtensionApiTest::SetUpCommandLine(command_line);
+ command_line->AppendSwitchASCII(
+ extensions::switches::kAllowlistedExtensionID, kAllowlistedExtensionId);
+ }
+
+ void SetUpOnMainThread() override {
+ ExtensionApiTest::SetUpOnMainThread();
+
+ // Record every request that actually reaches the server, so that tests can
+ // assert a rejected FollowRedirect() never made it onto the wire. Must be
+ // registered before the server starts; runs on the test server's thread.
+ embedded_test_server()->RegisterRequestMonitor(base::BindRepeating(
+ &WebRequestProxyingURLLoaderFactoryFollowRedirectTest::MonitorRequest,
+ base::Unretained(this)));
+ ASSERT_TRUE(StartEmbeddedTestServer());
+
+ // Proxy requests even when no extension has registered a listener yet, so
+ // that the tests which don't load an extension still exercise the proxy.
+ ForceProxy();
+
+ ASSERT_TRUE(NavigateToURL(GetActiveWebContents(),
+ embedded_test_server()->GetURL("/simple.html")));
+ initiator_origin_ =
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin();
+ }
+
+ protected:
+ void ForceProxy() {
+ auto* web_request_api =
+ BrowserContextKeyedAPIFactory<WebRequestAPI>::Get(profile());
+ ASSERT_TRUE(web_request_api);
+ web_request_api->ForceProxyForTesting();
+ profile()->GetDefaultStoragePartition()->FlushNetworkInterfaceForTesting();
+ }
+
+ // Binds a URLLoaderFactory equivalent to the one the renderer holds, i.e.
+ // one wrapped by WebRequestProxyingURLLoaderFactory. Must be called after any
+ // extension is loaded, since loading an extension re-creates the factory.
+ mojo::Remote<network::mojom::URLLoaderFactory> CreateFrameFactory() {
+ mojo::Remote<network::mojom::URLLoaderFactory> factory;
+ EXPECT_TRUE(GetActiveWebContents()
+ ->GetPrimaryMainFrame()
+ ->CreateNetworkServiceDefaultFactory(
+ factory.BindNewPipeAndPassReceiver()));
+ return factory;
+ }
+
+ void StartRequest(network::mojom::URLLoaderFactory* factory,
+ mojo::Remote<network::mojom::URLLoader>& loader,
+ network::TestURLLoaderClient& client,
+ const GURL& url) {
+ network::ResourceRequest request;
+ request.url = url;
+ request.request_initiator = initiator_origin_;
+ factory->CreateLoaderAndStart(
+ loader.BindNewPipeAndPassReceiver(), next_request_id_++,
+ network::mojom::kURLLoadOptionNone, request, client.CreateRemote(),
+ net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+ }
+
+ // Returns a URL which server-redirects to `target`.
+ GURL RedirectingUrlTo(const GURL& target) {
+ return embedded_test_server()->GetURL("/server-redirect?" + target.spec());
+ }
+
+ bool DidReachNetwork(const std::string& relative_url) {
+ base::AutoLock lock(lock_);
+ return observed_relative_urls_.contains(relative_url);
+ }
+
+ private:
+ void MonitorRequest(const net::test_server::HttpRequest& request) {
+ base::AutoLock lock(lock_);
+ observed_relative_urls_.insert(request.relative_url);
+ }
+
+ url::Origin initiator_origin_;
+ int32_t next_request_id_ = 1;
+ base::Lock lock_;
+ std::set<std::string> observed_relative_urls_ GUARDED_BY(lock_);
+};
+
+// Verifies that calls to FollowRedirect() that are unexpected (either in their
+// arguments or ordering) are rejected.
+IN_PROC_BROWSER_TEST_F(WebRequestProxyingURLLoaderFactoryFollowRedirectTest,
+ BadFollowRedirectCallsAreRejected) {
+ mojo::Remote<network::mojom::URLLoaderFactory> factory = CreateFrameFactory();
+ const GURL echo_url = embedded_test_server()->GetURL("/echo");
+ const GURL redirect_url = RedirectingUrlTo(echo_url);
+
+ // 1. Calling FollowRedirect() when no redirect is pending.
+ {
+ mojo::test::BadMessageObserver bad_message_observer;
+ network::TestURLLoaderClient client;
+ mojo::Remote<network::mojom::URLLoader> loader;
+ StartRequest(factory.get(), loader, client, echo_url);
+
+ loader->FollowRedirect(network::HttpRequestHeadersUpdateParams(),
+ std::nullopt);
+
+ EXPECT_EQ("Unexpected FollowRedirect",
+ bad_message_observer.WaitForBadMessage());
+ client.RunUntilComplete();
+ EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client.completion_status().error_code);
+ }
+
+ // 2. Calling FollowRedirect() twice sequentially for a single redirect.
+ // Verifies that `deferred_redirect_url_` is properly reset after the first
+ // follow and cannot be followed again.
+ {
+ mojo::test::BadMessageObserver bad_message_observer;
+ network::TestURLLoaderClient client;
+ mojo::Remote<network::mojom::URLLoader> loader;
+ StartRequest(factory.get(), loader, client, redirect_url);
+
+ client.RunUntilRedirectReceived();
+ ASSERT_TRUE(client.has_received_redirect());
+
+ loader->FollowRedirect(network::HttpRequestHeadersUpdateParams(),
+ std::nullopt);
+ loader->FollowRedirect(network::HttpRequestHeadersUpdateParams(),
+ std::nullopt);
+
+ EXPECT_EQ("Unexpected FollowRedirect",
+ bad_message_observer.WaitForBadMessage());
+ client.RunUntilComplete();
+ EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client.completion_status().error_code);
+ }
+
+ // 3. Calling FollowRedirect() with a cross-origin `new_url`.
+ {
+ mojo::test::BadMessageObserver bad_message_observer;
+ network::TestURLLoaderClient client;
+ mojo::Remote<network::mojom::URLLoader> loader;
+ StartRequest(factory.get(), loader, client, redirect_url);
+
+ client.RunUntilRedirectReceived();
+ ASSERT_TRUE(client.has_received_redirect());
+
+ loader->FollowRedirect(network::HttpRequestHeadersUpdateParams(),
+ GURL("https://example.com/other"));
+
+ EXPECT_EQ("Unexpected new_url in FollowRedirect",
+ bad_message_observer.WaitForBadMessage());
+ client.RunUntilComplete();
... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/extensions/api/web_request/web_request_proxying_url_loader_factory_browsertest.cc b/chrome/browser/extensions/api/web_request/web_request_proxying_url_loader_factory_browsertest.cc
new file mode 100644
index 0000000..8635585
--- /dev/null
+++ b/chrome/browser/extensions/api/web_request/web_request_proxying_url_loader_factory_browsertest.cc
@@ -0,0 +1,364 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <optional>
+#include <set>
+#include <string>
+
+#include "base/command_line.h"
+#include "base/strings/stringprintf.h"
+#include "base/synchronization/lock.h"
+#include "chrome/browser/extensions/extension_apitest.h"
+#include "chrome/browser/profiles/profile.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/storage_partition.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "extensions/browser/api/web_request/web_request_api.h"
+#include "extensions/browser/browser_context_keyed_api_factory.h"
+#include "extensions/common/extension.h"
+#include "extensions/common/switches.h"
+#include "extensions/test/extension_test_message_listener.h"
+#include "extensions/test/test_extension_dir.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/url_loader.mojom.h"
+#include "services/network/public/mojom/url_loader_factory.mojom.h"
+#include "services/network/test/test_url_loader_client.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace extensions {
+
+namespace {
+
+// Key and ID for an allowlisted MV3 extension permitted to use the
+// "webRequestBlocking" permission.
+constexpr char kAllowlistedExtensionKey[] =
+ "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8xv6iO+j4kzj1HiBL93+XVJH/CRyAQMUHS/"
+ "Z0l8nCAzaAFkW/JsNwxJqQhrZspnxLqbQxNncXs6g6bsXAwKHiEs+LSs+bIv0Gc/2ycZdhXJ8G"
+ "hEsSMakog5dpQd1681c2gLK/8CrAoewE/0GIKhaFcp7a2iZlGh4Am6fgMKy0iQIDAQAB";
+constexpr char kAllowlistedExtensionId[] = "ddchlicdkolnonkihahngkmmmjnjlkkf";
+
+} // namespace
+
+// Test fixture for validation of URLLoader::FollowRedirect() in
+// WebRequestProxyingURLLoaderFactory. Regression coverage for
+// crbug.com/497494634, where a compromised renderer could send an unsolicited
+// FollowRedirect() to swap the request URL out from under a pending blocking
+// webRequest.onBeforeRequest listener, reaching a URL the listener never saw.
+//
+// These tests drive the frame's URLLoaderFactory directly, which is what a
+// compromised renderer has access to.
+class WebRequestProxyingURLLoaderFactoryFollowRedirectTest
+ : public ExtensionApiTest {
+ public:
+ WebRequestProxyingURLLoaderFactoryFollowRedirectTest() = default;
+ ~WebRequestProxyingURLLoaderFactoryFollowRedirectTest() override = default;
+
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ ExtensionApiTest::SetUpCommandLine(command_line);
+ command_line->AppendSwitchASCII(
+ extensions::switches::kAllowlistedExtensionID, kAllowlistedExtensionId);
+ }
+
+ void SetUpOnMainThread() override {
+ ExtensionApiTest::SetUpOnMainThread();
+
+ // Record every request that actually reaches the server, so that tests can
+ // assert a rejected FollowRedirect() never made it onto the wire. Must be
+ // registered before the server starts; runs on the test server's thread.
+ embedded_test_server()->RegisterRequestMonitor(base::BindRepeating(
+ &WebRequestProxyingURLLoaderFactoryFollowRedirectTest::MonitorRequest,
+ base::Unretained(this)));
+ ASSERT_TRUE(StartEmbeddedTestServer());
+
+ // Proxy requests even when no extension has registered a listener yet, so
+ // that the tests which don't load an extension still exercise the proxy.
+ ForceProxy();
+
+ ASSERT_TRUE(NavigateToURL(GetActiveWebContents(),
+ embedded_test_server()->GetURL("/simple.html")));
+ initiator_origin_ =
+ GetActiveWebContents()->GetPrimaryMainFrame()->GetLastCommittedOrigin();
+ }
+
+ protected:
+ void ForceProxy() {
+ auto* web_request_api =
+ BrowserContextKeyedAPIFactory<WebRequestAPI>::Get(profile());
+ ASSERT_TRUE(web_request_api);
+ web_request_api->ForceProxyForTesting();
+ profile()->GetDefaultStoragePartition()->FlushNetworkInterfaceForTesting();
+ }
+
+ // Binds a URLLoaderFactory equivalent to the one the renderer holds, i.e.
+ // one wrapped by WebRequestProxyingURLLoaderFactory. Must be called after any
+ // extension is loaded, since loading an extension re-creates the factory.
+ mojo::Remote<network::mojom::URLLoaderFactory> CreateFrameFactory() {
+ mojo::Remote<network::mojom::URLLoaderFactory> factory;
+ EXPECT_TRUE(GetActiveWebContents()
+ ->GetPrimaryMainFrame()
+ ->CreateNetworkServiceDefaultFactory(
+ factory.BindNewPipeAndPassReceiver()));
+ return factory;
+ }
+
+ void StartRequest(network::mojom::URLLoaderFactory* factory,
+ mojo::Remote<network::mojom::URLLoader>& loader,
+ network::TestURLLoaderClient& client,
+ const GURL& url) {
+ network::ResourceRequest request;
+ request.url = url;
+ request.request_initiator = initiator_origin_;
+ factory->CreateLoaderAndStart(
+ loader.BindNewPipeAndPassReceiver(), next_request_id_++,
+ network::mojom::kURLLoadOptionNone, request, client.CreateRemote(),
+ net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+ }
+
+ // Returns a URL which server-redirects to `target`.
+ GURL RedirectingUrlTo(const GURL& target) {
+ return embedded_test_server()->GetURL("/server-redirect?" + target.spec());
+ }
+
+ bool DidReachNetwork(const std::string& relative_url) {
+ base::AutoLock lock(lock_);
+ return observed_relative_urls_.contains(relative_url);
+ }
+
+ private:
+ void MonitorRequest(const net::test_server::HttpRequest& request) {
+ base::AutoLock lock(lock_);
+ observed_relative_urls_.insert(request.relative_url);
+ }
+
+ url::Origin initiator_origin_;
+ int32_t next_request_id_ = 1;
+ base::Lock lock_;
+ std::set<std::string> observed_relative_urls_ GUARDED_BY(lock_);
+};
+
+// Verifies that calls to FollowRedirect() that are unexpected (either in their
+// arguments or ordering) are rejected.
+IN_PROC_BROWSER_TEST_F(WebRequestProxyingURLLoaderFactoryFollowRedirectTest,
+ BadFollowRedirectCallsAreRejected) {
+ mojo::Remote<network::mojom::URLLoaderFactory> factory = CreateFrameFactory();
+ const GURL echo_url = embedded_test_server()->GetURL("/echo");
+ const GURL redirect_url = RedirectingUrlTo(echo_url);
+
+ // 1. Calling FollowRedirect() when no redirect is pending.
+ {
+ mojo::test::BadMessageObserver bad_message_observer;
+ network::TestURLLoaderClient client;
+ mojo::Remote<network::mojom::URLLoader> loader;
+ StartRequest(factory.get(), loader, client, echo_url);
+
+ loader->FollowRedirect(network::HttpRequestHeadersUpdateParams(),
+ std::nullopt);
+
+ EXPECT_EQ("Unexpected FollowRedirect",
+ bad_message_observer.WaitForBadMessage());
+ client.RunUntilComplete();
+ EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client.completion_status().error_code);
+ }
+
+ // 2. Calling FollowRedirect() twice sequentially for a single redirect.
+ // Verifies that `deferred_redirect_url_` is properly reset after the first
+ // follow and cannot be followed again.
+ {
+ mojo::test::BadMessageObserver bad_message_observer;
+ network::TestURLLoaderClient client;
+ mojo::Remote<network::mojom::URLLoader> loader;
+ StartRequest(factory.get(), loader, client, redirect_url);
+
+ client.RunUntilRedirectReceived();
+ ASSERT_TRUE(client.has_received_redirect());
+
+ loader->FollowRedirect(network::HttpRequestHeadersUpdateParams(),
+ std::nullopt);
+ loader->FollowRedirect(network::HttpRequestHeadersUpdateParams(),
+ std::nullopt);
+
+ EXPECT_EQ("Unexpected FollowRedirect",
+ bad_message_observer.WaitForBadMessage());
+ client.RunUntilComplete();
+ EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client.completion_status().error_code);
+ }
+
+ // 3. Calling FollowRedirect() with a cross-origin `new_url`.
+ {
+ mojo::test::BadMessageObserver bad_message_observer;
+ network::TestURLLoaderClient client;
+ mojo::Remote<network::mojom::URLLoader> loader;
+ StartRequest(factory.get(), loader, client, redirect_url);
+
+ client.RunUntilRedirectReceived();
+ ASSERT_TRUE(client.has_received_redirect());
+
+ loader->FollowRedirect(network::HttpRequestHeadersUpdateParams(),
+ GURL("https://example.com/other"));
+
+ EXPECT_EQ("Unexpected new_url in FollowRedirect",
+ bad_message_observer.WaitForBadMessage());
+ client.RunUntilComplete();
+ EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client.completion_status().error_code);
+ }
+
+ // 4. Calling FollowRedirect() with same-origin but credentialed `new_url`.
+ {
+ GURL::Replacements add_credentials;
+ add_credentials.SetUsernameStr("user");
+ add_credentials.SetPasswordStr("password");
+
+ mojo::test::BadMessageObserver bad_message_observer;
+ network::TestURLLoaderClient client;
+ mojo::Remote<network::mojom::URLLoader> loader;
+ StartRequest(factory.get(), loader, client, redirect_url);
+
+ client.RunUntilRedirectReceived();
+ ASSERT_TRUE(client.has_received_redirect());
+
+ loader->FollowRedirect(network::HttpRequestHeadersUpdateParams(),
+ echo_url.ReplaceComponents(add_credentials));
+
+ EXPECT_EQ("new_url with credentials in FollowRedirect",
+ bad_message_observer.WaitForBadMessage());
+ client.RunUntilComplete();
+ EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client.completion_status().error_code);
+ }
+}
+
+// Tests that using `new_url` to adjust the redirect to another URL in the same
+// origin is allowed (e.g., appending a query parameter). This is the intended
+// use of `new_url`.
+IN_PROC_BROWSER_TEST_F(WebRequestProxyingURLLoaderFactoryFollowRedirectTest,
+ FollowRedirectWithSameOriginNewUrlIsAllowed) {
+ mojo::Remote<network::mojom::URLLoaderFactory> factory = CreateFrameFactory();
+
+ const GURL announced_url = embedded_test_server()->GetURL("/echo?announced");
+ const GURL modified_url = embedded_test_server()->GetURL("/echo?modified");
+
+ network::TestURLLoaderClient client;
+ mojo::Remote<network::mojom::URLLoader> loader;
+ StartRequest(factory.get(), loader, client, RedirectingUrlTo(announced_url));
+
+ client.RunUntilRedirectReceived();
+ ASSERT_EQ(announced_url, client.redirect_info().new_url);
+ client.ClearHasReceivedRedirect();
+
+ loader->FollowRedirect(network::HttpRequestHeadersUpdateParams(),
+ modified_url);
+
+ // Supplying `new_url` does not silently rewrite the destination: the network
+ // stack synthesizes an internal redirect to it, which the proxy forwards on
+ // as a normal redirect (giving extensions a chance to see the new URL). The
+ // client is expected to follow that one too.
+ client.RunUntilRedirectReceived();
+ ASSERT_EQ(modified_url, client.redirect_info().new_url);
+ client.ClearHasReceivedRedirect();
+
+ loader->FollowRedirect(network::HttpRequestHeadersUpdateParams(),
+ std::nullopt);
+
+ client.RunUntilComplete();
+ EXPECT_EQ(net::OK, client.completion_status().error_code);
+
+ // The modified destination is fetched.
+ EXPECT_TRUE(DidReachNetwork("/echo?modified"));
+ EXPECT_FALSE(DidReachNetwork("/echo?announced"));
+}
+
+// Tests that an unexpected FollowRedirect() cannot route a request to a URL
+// that a blocking onBeforeRequest listener would have cancelled.
+// (Regression test for the specific scenario from crbug.com/497494634.)
+IN_PROC_BROWSER_TEST_F(WebRequestProxyingURLLoaderFactoryFollowRedirectTest,
+ UnexpectedFollowRedirectCannotBypassBlockingListener) {
+ TestExtensionDir test_dir;
+ test_dir.WriteManifest(base::StringPrintf(
+ R"({
+ "name": "Policy Filter Extension",
+ "version": "1.0",
+ "manifest_version": 3,
+ "key": "%s",
+ "permissions": ["webRequest", "webRequestBlocking"],
+ "host_permissions": ["<all_urls>"],
+ "background": {"service_worker": "background.js"}
+ })",
+ kAllowlistedExtensionKey));
... (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.
References
On This Page