Close HTTP/1.1 sockets when blocked by CORB or CORP.

BUG=1154250

Change-Id: Iff9b03523b6265cb7e5ccc0bbbc43dd911ac9b9b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2575014
Reviewed-by: Shivani Sharma <[email protected]>
Reviewed-by: Ɓukasz Anforowicz <[email protected]>
Commit-Queue: Matt Menke <[email protected]>
Cr-Commit-Position: refs/heads/master@{#834909}
diff --git a/net/http/http_cache_transaction.cc b/net/http/http_cache_transaction.cc
index 3ee8cba..27f9663 100644
--- a/net/http/http_cache_transaction.cc
+++ b/net/http/http_cache_transaction.cc
@@ -615,6 +615,14 @@
               network_transaction_info_.old_connection_attempts.end());
 }
 
+void HttpCache::Transaction::CloseConnectionOnDestruction() {
+  if (network_trans_) {
+    network_trans_->CloseConnectionOnDestruction();
+  } else if (InWriters()) {
+    entry_->writers->CloseConnectionOnDestruction();
+  }
+}
+
 void HttpCache::Transaction::SetValidatingCannotProceed() {
   DCHECK(!reading_);
   // Ensure this transaction is waiting for a callback.
diff --git a/net/http/http_cache_transaction.h b/net/http/http_cache_transaction.h
index 1d76d0197..63baba5 100644
--- a/net/http/http_cache_transaction.h
+++ b/net/http/http_cache_transaction.h
@@ -160,6 +160,7 @@
   void SetResponseHeadersCallback(ResponseHeadersCallback callback) override;
   int ResumeNetworkStart() override;
   void GetConnectionAttempts(ConnectionAttempts* out) const override;
+  void CloseConnectionOnDestruction() override;
 
   // Invoked when parallel validation cannot proceed due to response failure
   // and this transaction needs to be restarted.
diff --git a/net/http/http_cache_writers.cc b/net/http/http_cache_writers.cc
index 9459509..a80dc43 100644
--- a/net/http/http_cache_writers.cc
+++ b/net/http/http_cache_writers.cc
@@ -225,6 +225,11 @@
   }
 }
 
+void HttpCache::Writers::CloseConnectionOnDestruction() {
+  if (network_transaction_)
+    network_transaction_->CloseConnectionOnDestruction();
+}
+
 bool HttpCache::Writers::ContainsOnlyIdleWriters() const {
   return waiting_for_read_.empty() && !active_transaction_;
 }
diff --git a/net/http/http_cache_writers.h b/net/http/http_cache_writers.h
index c638aeb..4da9f1e 100644
--- a/net/http/http_cache_writers.h
+++ b/net/http/http_cache_writers.h
@@ -125,6 +125,8 @@
     return network_transaction_.get();
   }
 
+  void CloseConnectionOnDestruction();
+
   // Returns the load state of the |network_transaction_| if present else
   // returns LOAD_STATE_IDLE.
   LoadState GetLoadState() const;
diff --git a/net/http/http_network_transaction.cc b/net/http/http_network_transaction.cc
index af1020cc..e526512 100644
--- a/net/http/http_network_transaction.cc
+++ b/net/http/http_network_transaction.cc
@@ -148,7 +148,8 @@
     // TODO(mbelshe): The stream_ should be able to compute whether or not the
     //                stream should be kept alive.  No reason to compute here
     //                and pass it in.
-    if (!stream_->CanReuseConnection() || next_state_ != STATE_NONE) {
+    if (!stream_->CanReuseConnection() || next_state_ != STATE_NONE ||
+        close_connection_on_destruction_) {
       stream_->Close(true /* not reusable */);
     } else if (stream_->IsResponseBodyComplete()) {
       // If the response body is complete, we can just reuse the socket.
@@ -542,6 +543,10 @@
   return DoLoop(OK);
 }
 
+void HttpNetworkTransaction::CloseConnectionOnDestruction() {
+  close_connection_on_destruction_ = true;
+}
+
 void HttpNetworkTransaction::OnStreamReady(const SSLConfig& used_ssl_config,
                                            const ProxyInfo& used_proxy_info,
                                            std::unique_ptr<HttpStream> stream) {
diff --git a/net/http/http_network_transaction.h b/net/http/http_network_transaction.h
index c0fc39f..f95473ea 100644
--- a/net/http/http_network_transaction.h
+++ b/net/http/http_network_transaction.h
@@ -88,8 +88,8 @@
   void SetConnectedCallback(const ConnectedCallback& callback) override;
   void SetRequestHeadersCallback(RequestHeadersCallback callback) override;
   void SetResponseHeadersCallback(ResponseHeadersCallback callback) override;
-
   int ResumeNetworkStart() override;
+  void CloseConnectionOnDestruction() override;
 
   // HttpStreamRequest::Delegate methods:
   void OnStreamReady(const SSLConfig& used_ssl_config,
@@ -435,6 +435,8 @@
   // Number of times the transaction was restarted via a RestartWith* call.
   size_t num_restarts_;
 
+  bool close_connection_on_destruction_ = false;
+
   DISALLOW_COPY_AND_ASSIGN(HttpNetworkTransaction);
 };
 
diff --git a/net/http/http_network_transaction_unittest.cc b/net/http/http_network_transaction_unittest.cc
index a63c0c08..df4fda9 100644
--- a/net/http/http_network_transaction_unittest.cc
+++ b/net/http/http_network_transaction_unittest.cc
@@ -9390,6 +9390,105 @@
   EXPECT_EQ(1, GetIdleSocketCountInTransportSocketPool(session.get()));
 }
 
+TEST_F(HttpNetworkTransactionTest, CloseConnectionOnDestruction) {
+  enum class TestCase {
+    kReadHeaders,
+    kReadPartOfBodyRead,
+    kReadAllOfBody,
+  };
+
+  for (auto test_case : {TestCase::kReadHeaders, TestCase::kReadPartOfBodyRead,
+                         TestCase::kReadAllOfBody}) {
+    SCOPED_TRACE(testing::Message()
+                 << "Test case: " << static_cast<int>(test_case));
+    for (bool close_connection : {false, true}) {
+      if (test_case != TestCase::kReadAllOfBody || close_connection == false)
+        continue;
+      SCOPED_TRACE(testing::Message()
+                   << "Close connection: " << close_connection);
+
+      HttpRequestInfo request;
+      request.method = "GET";
+      request.url = GURL("http://foo.test/");
+      request.traffic_annotation =
+          net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS);
+
+      std::unique_ptr<HttpNetworkSession> session(
+          CreateSession(&session_deps_));
+
+      std::unique_ptr<HttpNetworkTransaction> trans =
+          std::make_unique<HttpNetworkTransaction>(DEFAULT_PRIORITY,
+                                                   session.get());
+
+      MockRead data_reads[] = {
+          // A part of the response body is received with the response headers.
+          MockRead("HTTP/1.1 200 OK\r\n"
+                   "Content-Length: 11\r\n\r\n"
+                   "hello world"),
+          MockRead(SYNCHRONOUS, OK),
+      };
+
+      StaticSocketDataProvider data(data_reads, base::span<MockWrite>());
+      session_deps_.socket_factory->AddSocketDataProvider(&data);
+
+      TestCompletionCallback callback;
+
+      int rv = trans->Start(&request, callback.callback(), NetLogWithSource());
+      EXPECT_THAT(callback.GetResult(rv), IsOk());
+
+      const HttpResponseInfo* response = trans->GetResponseInfo();
+      ASSERT_TRUE(response);
+
+      EXPECT_TRUE(response->headers);
+      std::string status_line = response->headers->GetStatusLine();
+      EXPECT_EQ("HTTP/1.1 200 OK", status_line);
+
+      EXPECT_EQ(0, GetIdleSocketCountInTransportSocketPool(session.get()));
+
+      std::string response_data;
+      switch (test_case) {
+        case TestCase::kReadHeaders: {
+          // Already read the headers, nothing else to do.
+          break;
+        }
+
+        case TestCase::kReadPartOfBodyRead: {
+          scoped_refptr<IOBuffer> buf = base::MakeRefCounted<IOBuffer>(5);
+          rv = trans->Read(buf.get(), 5, callback.callback());
+          ASSERT_EQ(5, callback.GetResult(rv));
+          response_data.assign(buf->data(), 5);
+          EXPECT_EQ("hello", response_data);
+          break;
+        }
+
+        case TestCase::kReadAllOfBody: {
+          rv = ReadTransaction(trans.get(), &response_data);
+          EXPECT_THAT(rv, IsOk());
+          EXPECT_EQ("hello world", response_data);
+          break;
+        }
+      }
+
+      if (close_connection)
+        trans->CloseConnectionOnDestruction();
+      trans.reset();
+
+      // Wait for the socket to be drained and added to the socket pool or
+      // destroyed.
+      base::RunLoop().RunUntilIdle();
+
+      // In the case all the body was read, the socket will have been released
+      // before the CloseConnectionOnDestruction() call, so will not be
+      // destroyed.
+      if (close_connection && test_case != TestCase::kReadAllOfBody) {
+        EXPECT_EQ(0, GetIdleSocketCountInTransportSocketPool(session.get()));
+      } else {
+        EXPECT_EQ(1, GetIdleSocketCountInTransportSocketPool(session.get()));
+      }
+    }
+  }
+}
+
 // Grab a socket, use it, and put it back into the pool. Then, make
 // low memory notification and ensure the socket pool is flushed.
 TEST_F(HttpNetworkTransactionTest, FlushSocketPoolOnLowMemoryNotifications) {
diff --git a/net/http/http_transaction.h b/net/http/http_transaction.h
index 19e4d4d..e3a44d98 100644
--- a/net/http/http_transaction.h
+++ b/net/http/http_transaction.h
@@ -206,6 +206,19 @@
   virtual int ResumeNetworkStart() = 0;
 
   virtual void GetConnectionAttempts(ConnectionAttempts* out) const = 0;
+
+  // Configures the transaction to close the network connection, if any, on
+  // destruction. Intended for cases where keeping the socket alive may leak
+  // data. Does not immediately close the socket. If multiple transactions are
+  // using the same socket, only closes it once all transactions have completed.
+  //
+  // Does not close H2/H3 sessions, but does close H1 tunnels on top of H2/H3
+  // sessions.
+  //
+  // Only applies to currently in-use connections. Does nothing after the last
+  // byte of the response body has been read, as the connection is no longer in
+  // use at that point.
+  virtual void CloseConnectionOnDestruction() = 0;
 };
 
 }  // namespace net
diff --git a/net/http/http_transaction_test_util.cc b/net/http/http_transaction_test_util.cc
index 2e4b0ef..01f0271e8 100644
--- a/net/http/http_transaction_test_util.cc
+++ b/net/http/http_transaction_test_util.cc
@@ -572,6 +572,10 @@
   NOTIMPLEMENTED();
 }
 
+void MockNetworkTransaction::CloseConnectionOnDestruction() {
+  NOTIMPLEMENTED();
+}
+
 void MockNetworkTransaction::CallbackLater(CompletionOnceCallback callback,
                                            int result) {
   base::ThreadTaskRunnerHandle::Get()->PostTask(
diff --git a/net/http/http_transaction_test_util.h b/net/http/http_transaction_test_util.h
index 89a5826..aa7be5c3 100644
--- a/net/http/http_transaction_test_util.h
+++ b/net/http/http_transaction_test_util.h
@@ -250,6 +250,8 @@
 
   void GetConnectionAttempts(ConnectionAttempts* out) const override;
 
+  void CloseConnectionOnDestruction() override;
+
   CreateHelper* websocket_handshake_stream_create_helper() {
     return websocket_handshake_stream_create_helper_;
   }
diff --git a/net/url_request/url_request.cc b/net/url_request/url_request.cc
index b8409e8..db6f60d 100644
--- a/net/url_request/url_request.cc
+++ b/net/url_request/url_request.cc
@@ -866,6 +866,14 @@
   job_->ContinueDespiteLastError();
 }
 
+void URLRequest::AbortAndCloseConnection() {
+  DCHECK_EQ(OK, status_);
+  DCHECK(!has_notified_completion_);
+  DCHECK(job_);
+  job_->CloseConnectionOnDestruction();
+  job_.reset();
+}
+
 void URLRequest::PrepareToRestart() {
   DCHECK(job_.get());
 
diff --git a/net/url_request/url_request.h b/net/url_request/url_request.h
index eed0ae9..a640343 100644
--- a/net/url_request/url_request.h
+++ b/net/url_request/url_request.h
@@ -618,6 +618,24 @@
   // cancel the request instead, call Cancel().
   void ContinueDespiteLastError();
 
+  // Aborts the request (without invoking any completion callbacks) and closes
+  // the current connection, rather than returning it to the socket pool. Only
+  // affects HTTP/1.1 connections and tunnels.
+  //
+  // Intended to be used in cases where socket reuse can potentially leak data
+  // across sites.
+  //
+  // May only be called after Delegate::OnResponseStarted() has been invoked
+  // with net::OK, but before the body has been completely read. After the last
+  // body has been read, the socket may have already been handed off to another
+  // consumer.
+  //
+  // Due to transactions potentially being shared by multiple URLRequests in
+  // some cases, it is possible the socket may not be immediately closed, but
+  // will instead be closed when all URLRequests sharing the socket have been
+  // destroyed.
+  void AbortAndCloseConnection();
+
   // Used to specify the context (cookie store, cache) for this request.
   const URLRequestContext* context() const;
 
diff --git a/net/url_request/url_request_context_builder.cc b/net/url_request/url_request_context_builder.cc
index d19aaf4..d6f1215 100644
--- a/net/url_request/url_request_context_builder.cc
+++ b/net/url_request/url_request_context_builder.cc
@@ -541,6 +541,10 @@
 
   HttpNetworkSession::Context network_session_context;
   SetHttpNetworkSessionComponents(context.get(), &network_session_context);
+  // Unlike the other fields of HttpNetworkSession::Context,
+  // |client_socket_factory| is not mirrored in URLRequestContext.
+  network_session_context.client_socket_factory =
+      client_socket_factory_for_testing_;
 
   storage->set_http_network_session(std::make_unique<HttpNetworkSession>(
       http_network_session_params_, network_session_context));
diff --git a/net/url_request/url_request_context_builder.h b/net/url_request/url_request_context_builder.h
index 713c74c0..26fcf0e 100644
--- a/net/url_request/url_request_context_builder.h
+++ b/net/url_request/url_request_context_builder.h
@@ -51,6 +51,7 @@
 namespace net {
 
 class CertVerifier;
+class ClientSocketFactory;
 class CookieStore;
 class CTPolicyEnforcer;
 class HttpAuthHandlerFactory;
@@ -311,6 +312,13 @@
       CreateHttpTransactionFactoryCallback
           create_http_network_transaction_factory);
 
+  // Sets a ClientSocketFactory so a test can mock out sockets. The
+  // ClientSocketFactory must be destroyed after the creates URLRequestContext.
+  void set_client_socket_factory_for_testing(
+      ClientSocketFactory* client_socket_factory_for_testing) {
+    client_socket_factory_for_testing_ = client_socket_factory_for_testing;
+  }
+
   // Creates a mostly self-contained URLRequestContext. May only be called once
   // per URLRequestContextBuilder. After this is called, the Builder can be
   // safely destroyed.
@@ -378,6 +386,8 @@
   std::map<std::string, std::unique_ptr<URLRequestJobFactory::ProtocolHandler>>
       protocol_handlers_;
 
+  ClientSocketFactory* client_socket_factory_for_testing_ = nullptr;
+
   DISALLOW_COPY_AND_ASSIGN(URLRequestContextBuilder);
 };
 
diff --git a/net/url_request/url_request_http_job.cc b/net/url_request/url_request_http_job.cc
index 2c1daf73..cc0608a 100644
--- a/net/url_request/url_request_http_job.cc
+++ b/net/url_request/url_request_http_job.cc
@@ -305,6 +305,11 @@
     out->clear();
 }
 
+void URLRequestHttpJob::CloseConnectionOnDestruction() {
+  DCHECK(transaction_);
+  transaction_->CloseConnectionOnDestruction();
+}
+
 int URLRequestHttpJob::NotifyConnectedCallback(const TransportInfo& info) {
   return URLRequestJob::NotifyConnected(info);
 }
diff --git a/net/url_request/url_request_http_job.h b/net/url_request/url_request_http_job.h
index 274314b..c026834 100644
--- a/net/url_request/url_request_http_job.h
+++ b/net/url_request/url_request_http_job.h
@@ -63,6 +63,7 @@
   void Start() override;
   void Kill() override;
   void GetConnectionAttempts(ConnectionAttempts* out) const override;
+  void CloseConnectionOnDestruction() override;
   std::unique_ptr<SourceStream> SetUpSourceStream() override;
 
   RequestPriority priority() const {
diff --git a/net/url_request/url_request_job.cc b/net/url_request/url_request_job.cc
index 1f669a4..9eaf309 100644
--- a/net/url_request/url_request_job.cc
+++ b/net/url_request/url_request_job.cc
@@ -270,6 +270,8 @@
   out->clear();
 }
 
+void URLRequestJob::CloseConnectionOnDestruction() {}
+
 namespace {
 
 // Assuming |url| has already been stripped for use as a referrer, if
diff --git a/net/url_request/url_request_job.h b/net/url_request/url_request_job.h
index fe4dc8ea..065794b 100644
--- a/net/url_request/url_request_job.h
+++ b/net/url_request/url_request_job.h
@@ -240,6 +240,10 @@
   // from the remote party with the actual response headers recieved.
   virtual void SetResponseHeadersCallback(ResponseHeadersCallback callback) {}
 
+  // Causes the current transaction always close its active socket on
+  // destruction. Does not close H2/H3 sessions.
+  virtual void CloseConnectionOnDestruction();
+
   // Given |policy|, |original_referrer|, and |destination|, returns the
   // referrer URL mandated by |request|'s referrer policy.
   //
diff --git a/services/network/throttling/throttling_network_transaction.cc b/services/network/throttling/throttling_network_transaction.cc
index 589ec47..ea61af7 100644
--- a/services/network/throttling/throttling_network_transaction.cc
+++ b/services/network/throttling/throttling_network_transaction.cc
@@ -304,4 +304,8 @@
   network_transaction_->GetConnectionAttempts(out);
 }
 
+void ThrottlingNetworkTransaction::CloseConnectionOnDestruction() {
+  network_transaction_->CloseConnectionOnDestruction();
+}
+
 }  // namespace network
diff --git a/services/network/throttling/throttling_network_transaction.h b/services/network/throttling/throttling_network_transaction.h
index a9adb44..83b510a8 100644
--- a/services/network/throttling/throttling_network_transaction.h
+++ b/services/network/throttling/throttling_network_transaction.h
@@ -85,6 +85,7 @@
       net::ResponseHeadersCallback callback) override;
   int ResumeNetworkStart() override;
   void GetConnectionAttempts(net::ConnectionAttempts* out) const override;
+  void CloseConnectionOnDestruction() override;
 
  protected:
   friend class ThrottlingControllerTestHelper;
diff --git a/services/network/url_loader.cc b/services/network/url_loader.cc
index 3273379d..c92b8a07 100644
--- a/services/network/url_loader.cc
+++ b/services/network/url_loader.cc
@@ -1122,6 +1122,18 @@
               coep_reporter_)) {
     CompleteBlockedResponse(net::ERR_BLOCKED_BY_RESPONSE, false,
                             blocked_reason);
+    // TODO(https://crbug.com/1154250):  Close the socket here.
+    // For more details see https://crbug.com/1154250#c17.
+    // Item 2 discusses redirect handling.
+    //
+    // "url_request_->AbortAndCloseConnection()" should ideally close the
+    // socket, but unfortunately, URLRequestHttpJob caches redirects in a way
+    // that ignores their response bodies, since they'll never be read. It does
+    // this by calling HttpCache::Transaction::StopCaching(), which also has the
+    // effect of detaching the HttpNetworkTransaction, which owns the socket,
+    // from the HttpCache::Transaction. To fix this, we'd either need to call
+    // StopCaching() later in the process, or make the HttpCache::Transaction
+    // continue to hang onto the HttpNetworkTransaction after this call.
     DeleteSelf();
     return;
   }
@@ -1352,6 +1364,9 @@
               coep_reporter_)) {
     CompleteBlockedResponse(net::ERR_BLOCKED_BY_RESPONSE, false,
                             blocked_reason);
+    // Close the socket associated with the request, to prevent leaking
+    // information.
+    url_request_->AbortAndCloseConnection();
     DeleteSelf();
     return;
   }
@@ -2070,6 +2085,11 @@
     // Ask the caller to continue processing the request.
     return kContinueRequest;
   }
+
+  // Close the socket associated with the request, to prevent leaking
+  // information.
+  url_request_->AbortAndCloseConnection();
+
   // Delete self and cancel the request - the caller doesn't need to continue.
   //
   // DeleteSelf is posted asynchronously, to make sure that the callers (e.g.
diff --git a/services/network/url_loader_unittest.cc b/services/network/url_loader_unittest.cc
index bc2edbde..817637f8 100644
--- a/services/network/url_loader_unittest.cc
+++ b/services/network/url_loader_unittest.cc
@@ -57,6 +57,7 @@
 #include "net/dns/mock_host_resolver.h"
 #include "net/http/http_response_info.h"
 #include "net/proxy_resolution/configured_proxy_resolution_service.h"
+#include "net/socket/socket_test_util.h"
 #include "net/ssl/client_cert_identity_test_util.h"
 #include "net/test/cert_test_util.h"
 #include "net/test/embedded_test_server/controllable_http_response.h"
@@ -492,13 +493,19 @@
         net::GetTestCertsDirectory().AppendASCII("quic-root.pem"));
 
     net::QuicSimpleTestServer::Start();
+    net::URLRequestFailedJob::AddUrlHandler();
+  }
+  ~URLLoaderTest() override {
+    net::URLRequestFilter::GetInstance()->ClearHandlers();
+  }
+
+  void SetUp() override {
     net::HttpNetworkSession::Params params;
     auto quic_context = std::make_unique<net::QuicContext>();
     quic_context->params()->origins_to_force_quic_on.insert(
         net::HostPortPair(net::QuicSimpleTestServer::GetHost(),
                           net::QuicSimpleTestServer::GetPort()));
     params.enable_quic = true;
-
     net::URLRequestContextBuilder context_builder;
     context_builder.set_http_network_session_params(params);
     context_builder.set_quic_context(std::move(quic_context));
@@ -507,17 +514,12 @@
     auto test_network_delegate = std::make_unique<net::TestNetworkDelegate>();
     unowned_test_network_delegate_ = test_network_delegate.get();
     context_builder.set_network_delegate(std::move(test_network_delegate));
+    context_builder.set_client_socket_factory_for_testing(GetSocketFactory());
     context_ = context_builder.Build();
     resource_scheduler_client_ = base::MakeRefCounted<ResourceSchedulerClient>(
         kProcessId, kRouteId, &resource_scheduler_,
         context_->network_quality_estimator());
-    net::URLRequestFailedJob::AddUrlHandler();
-  }
-  ~URLLoaderTest() override {
-    net::URLRequestFilter::GetInstance()->ClearHandlers();
-  }
 
-  void SetUp() override {
     test_server_.AddDefaultHandlers(
         base::FilePath(FILE_PATH_LITERAL("services/test/data")));
     // This Unretained is safe because test_server_ is owned by |this|.
@@ -534,18 +536,33 @@
         mock_resolver_proc.get());
   }
 
-  void TearDown() override { net::QuicSimpleTestServer::Shutdown(); }
+  void TearDown() override {
+    context_.reset();
+    net::QuicSimpleTestServer::Shutdown();
+  }
 
-  // Attempts to load |url| and returns the resulting error code. If |body| is
-  // non-NULL, also attempts to read the response body. The advantage of using
-  // |body| instead of calling ReadBody() after Load is that it will load the
-  // response body before URLLoader is complete, so URLLoader completion won't
-  // block on trying to write the body buffer.
+  // Attempts to load |url| and returns the resulting error code.
   int Load(const GURL& url, std::string* body = nullptr) WARN_UNUSED_RESULT {
     DCHECK(!ran_);
 
     ResourceRequest request =
         CreateResourceRequest(!request_body_ ? "GET" : "POST", url);
+
+    if (request_body_)
+      request.request_body = request_body_;
+
+    request.trusted_params->client_security_state.Swap(
+        &request_client_security_state_);
+    return LoadRequest(request, body);
+  }
+
+  // Attempts to load |request| and returns the resulting error code. If |body|
+  // is non-NULL, also attempts to read the response body. The advantage of
+  // using |body| instead of calling ReadBody() after Load is that it will load
+  // the response body before URLLoader is complete, so URLLoader completion
+  // won't block on trying to write the body buffer.
+  int LoadRequest(const ResourceRequest& request,
+                  std::string* body = nullptr) WARN_UNUSED_RESULT {
     uint32_t options = mojom::kURLLoadOptionNone;
     if (send_ssl_with_response_)
       options |= mojom::kURLLoadOptionSendSSLInfoWithResponse;
@@ -562,22 +579,16 @@
           ignore_last_upload_file_);
     }
 
-    if (request_body_)
-      request.request_body = request_body_;
-
-    request.trusted_params->client_security_state.Swap(
-        &request_client_security_state_);
-
     base::RunLoop delete_run_loop;
     mojo::Remote<mojom::URLLoader> loader;
     std::unique_ptr<URLLoader> url_loader;
 
     mojom::URLLoaderFactoryParams params;
     params.process_id = mojom::kBrowserProcessId;
-    params.is_corb_enabled = false;
+    params.is_corb_enabled = corb_enabled_;
     params.client_security_state.Swap(&factory_client_security_state_);
 
-    url::Origin origin = url::Origin::Create(url);
+    url::Origin origin = url::Origin::Create(request.url);
     params.isolation_info =
         net::IsolationInfo::CreateForInternalRequest(origin);
     params.is_trusted = true;
@@ -858,6 +869,9 @@
   // execute once a request reaches the test server.
   virtual void OnServerReceivedRequest(const net::test_server::HttpRequest&) {}
 
+  // Lets subclasses inject a mock ClientSocketFactory.
+  virtual net::ClientSocketFactory* GetSocketFactory() { return nullptr; }
+
  protected:
   void Monitor(const net::test_server::HttpRequest& request) {
     sent_request_ = request;
@@ -885,6 +899,8 @@
   mojom::ClientSecurityStatePtr request_client_security_state_;
   scoped_refptr<ResourceRequestBody> request_body_;
 
+  bool corb_enabled_ = false;
+
   // Used to ensure that methods are called either before or after a request is
   // made, since the test fixture is meant to be used only once.
   bool ran_ = false;
@@ -892,6 +908,20 @@
   TestURLLoaderClient client_;
 };
 
+class URLLoaderMockSocketTest : public URLLoaderTest {
+ public:
+  URLLoaderMockSocketTest() = default;
+  ~URLLoaderMockSocketTest() override = default;
+
+  // Lets subclasses inject mock ClientSocketFactories.
+  net::ClientSocketFactory* GetSocketFactory() override {
+    return &socket_factory_;
+  }
+
+ protected:
+  net::MockClientSocketFactory socket_factory_;
+};
+
 constexpr int URLLoaderTest::kProcessId;
 constexpr int URLLoaderTest::kRouteId;
 
@@ -6073,4 +6103,221 @@
   ASSERT_FALSE(network_service_client.client_security_state());
 }
 
+TEST_F(URLLoaderMockSocketTest,
+       CorbDoesNotCloseSocketsWhenResourcesNotBlocked) {
+  corb_enabled_ = true;
+
+  const net::MockWrite kWrites[] = {
+      net::MockWrite(net::SYNCHRONOUS, 0,
+                     "GET / HTTP/1.1\r\n"
+                     "Host: origin.test\r\n"
+                     "Connection: keep-alive\r\n"
+                     "User-Agent: \r\n"
+                     "Accept-Encoding: gzip, deflate\r\n\r\n"),
+  };
+  net::MockRead kReads[] = {
+      net::MockRead(net::SYNCHRONOUS, 1,
+                    "HTTP/1.1 200 OK\r\n"
+                    "Connection: keep-alive\r\n"
+                    "Content-Length: 5\r\n\r\n"),
+      net::MockRead(net::SYNCHRONOUS, 2, "Hello"),
+  };
+
+  net::SequencedSocketData socket_data(kReads, kWrites);
+  socket_factory_.AddSocketDataProvider(&socket_data);
+
+  GURL url("http://origin.test/");
+  url::Origin initiator =
+      url::Origin::Create(GURL("http://other-origin.test/"));
+
+  ResourceRequest request = CreateResourceRequest("GET", url);
+  request.mode = mojom::RequestMode::kCors;
+  request.request_initiator = initiator;
+  std::string body;
+  EXPECT_EQ(net::OK, LoadRequest(request, &body));
+  EXPECT_EQ(body, "Hello");
+
+  // Socket should still be alive, in the socket pool.
+  EXPECT_TRUE(socket_data.socket());
+}
+
+TEST_F(URLLoaderMockSocketTest, CorbClosesSocketOnReceivingHeaders) {
+  corb_enabled_ = true;
+
+  const net::MockWrite kWrites[] = {
+      net::MockWrite(net::SYNCHRONOUS, 0,
+                     "GET / HTTP/1.1\r\n"
+                     "Host: origin.test\r\n"
+                     "Connection: keep-alive\r\n"
+                     "User-Agent: \r\n"
+                     "Accept-Encoding: gzip, deflate\r\n\r\n"),
+  };
+  net::MockRead kReads[] = {
+      net::MockRead(net::SYNCHRONOUS, 1,
+                    "HTTP/1.1 200 OK\r\n"
+                    "Connection: keep-alive\r\n"
+                    "Cross-Origin-Resource-Policy: same-origin\r\n"
+                    "Content-Length: 23\r\n\r\n"),
+      net::MockRead(net::SYNCHRONOUS, 2, "This should not be read"),
+  };
+
+  net::SequencedSocketData socket_data(kReads, kWrites);
+  socket_factory_.AddSocketDataProvider(&socket_data);
+
+  GURL url("http://origin.test/");
+  url::Origin initiator =
+      url::Origin::Create(GURL("http://other-origin.test/"));
+
+  ResourceRequest request = CreateResourceRequest("GET", url);
+  request.mode = mojom::RequestMode::kCors;
+  request.request_initiator = initiator;
+  std::string body;
+  EXPECT_EQ(net::OK, LoadRequest(request, &body));
+  EXPECT_TRUE(body.empty());
+
+  // Socket should have been destroyed, so it will not be reused.
+  EXPECT_FALSE(socket_data.socket());
+}
+
+TEST_F(URLLoaderMockSocketTest,
+       CorbDoesNotCloseSocketsWhenResourcesNotBlockedAfterSniffingMimeType) {
+  corb_enabled_ = true;
+
+  const net::MockWrite kWrites[] = {
+      net::MockWrite(net::SYNCHRONOUS, 0,
+                     "GET / HTTP/1.1\r\n"
+                     "Host: origin.test\r\n"
+                     "Connection: keep-alive\r\n"
+                     "User-Agent: \r\n"
+                     "Accept-Encoding: gzip, deflate\r\n\r\n"),
+  };
+  net::MockRead kReads[] = {
+      net::MockRead(net::SYNCHRONOUS, 1,
+                    "HTTP/1.1 200 OK\r\n"
+                    "Connection: keep-alive\r\n"
+                    "Content-Type: application/json\r\n"
+                    "Content-Length: 17\r\n\r\n"),
+      net::MockRead(net::SYNCHRONOUS, 2, "Not actually JSON"),
+  };
+
+  net::SequencedSocketData socket_data(kReads, kWrites);
+  socket_factory_.AddSocketDataProvider(&socket_data);
+
+  GURL url("http://origin.test/");
+  url::Origin initiator =
+      url::Origin::Create(GURL("http://other-origin.test/"));
+
+  ResourceRequest request = CreateResourceRequest("GET", url);
+  request.mode = mojom::RequestMode::kCors;
+  request.request_initiator = initiator;
+  std::string body;
+  EXPECT_EQ(net::OK, LoadRequest(request, &body));
+  EXPECT_EQ("Not actually JSON", body);
+
+  // Socket should still be alive, in the socket pool.
+  EXPECT_TRUE(socket_data.socket());
+}
+
+TEST_F(URLLoaderMockSocketTest, CorbClosesSocketOnSniffingMimeType) {
+  corb_enabled_ = true;
+
+  const net::MockWrite kWrites[] = {
+      net::MockWrite(net::SYNCHRONOUS, 0,
+                     "GET / HTTP/1.1\r\n"
+                     "Host: origin.test\r\n"
+                     "Connection: keep-alive\r\n"
+                     "User-Agent: \r\n"
+                     "Accept-Encoding: gzip, deflate\r\n\r\n"),
+  };
+  net::MockRead kReads[] = {
+      net::MockRead(net::SYNCHRONOUS, 1,
+                    "HTTP/1.1 200 OK\r\n"
+                    "Connection: keep-alive\r\n"
+                    "Content-Type: application/json\r\n"
+                    "Content-Length: 9\r\n\r\n"),
+      net::MockRead(net::SYNCHRONOUS, 2, "{\"x\" : 3}"),
+  };
+
+  net::SequencedSocketData socket_data(kReads, kWrites);
+  socket_factory_.AddSocketDataProvider(&socket_data);
+
+  GURL url("http://origin.test/");
+  url::Origin initiator =
+      url::Origin::Create(GURL("http://other-origin.test/"));
+
+  ResourceRequest request = CreateResourceRequest("GET", url);
+  request.mode = mojom::RequestMode::kCors;
+  request.request_initiator = initiator;
+  std::string body;
+  EXPECT_EQ(net::OK, LoadRequest(request, &body));
+  EXPECT_TRUE(body.empty());
+
+  // Socket should have been destroyed, so it will not be reused.
+  EXPECT_FALSE(socket_data.socket());
+}
+
+TEST_F(URLLoaderMockSocketTest, CorpClosesSocket) {
+  auto client_security_state = NewSecurityState();
+  client_security_state->cross_origin_embedder_policy.value =
+      mojom::CrossOriginEmbedderPolicyValue::kRequireCorp;
+  client_security_state->private_network_request_policy =
+      mojom::PrivateNetworkRequestPolicy::kAllow;
+  set_factory_client_security_state(std::move(client_security_state));
+
+  const net::MockWrite kWrites[] = {
+      net::MockWrite(net::SYNCHRONOUS, 0,
+                     "GET / HTTP/1.1\r\n"
+                     "Host: origin.test\r\n"
+                     "Connection: keep-alive\r\n"
+                     "User-Agent: \r\n"
+                     "Accept-Encoding: gzip, deflate\r\n\r\n"),
+  };
+  net::MockRead kReads[] = {
+      net::MockRead(net::SYNCHRONOUS, 1,
+                    "HTTP/1.1 200 OK\r\n"
+                    "Connection: keep-alive\r\n"
+                    "Content-Type: test/plain\r\n"
+                    "Content-Length: 23\r\n\r\n"),
+      net::MockRead(net::SYNCHRONOUS, 2, "This should not be read"),
+  };
+
+  net::SequencedSocketData socket_data(kReads, kWrites);
+  socket_factory_.AddSocketDataProvider(&socket_data);
+
+  GURL url("http://origin.test/");
+  url::Origin initiator =
+      url::Origin::Create(GURL("http://other-origin.test/"));
+
+  ResourceRequest request = CreateResourceRequest("GET", url);
+  request.mode = mojom::RequestMode::kNoCors;
+  request.request_initiator = initiator;
+  EXPECT_EQ(net::ERR_BLOCKED_BY_RESPONSE, LoadRequest(request));
+
+  // Socket should have been destroyed, so it will not be reused.
+  EXPECT_FALSE(socket_data.socket());
+}
+
+TEST_F(URLLoaderMockSocketTest, PrivateNetworkRequestPolicyClosesSocket) {
+  auto client_security_state = NewSecurityState();
+  client_security_state->private_network_request_policy =
+      mojom::PrivateNetworkRequestPolicy::kBlockFromInsecureToMorePrivate;
+  set_factory_client_security_state(std::move(client_security_state));
+
+  // No data should be read or written. Trying to do so will assert.
+  net::SequencedSocketData socket_data;
+  socket_factory_.AddSocketDataProvider(&socket_data);
+
+  GURL url("http://origin.test/");
+  url::Origin initiator =
+      url::Origin::Create(GURL("http://other-origin.test/"));
+
+  ResourceRequest request = CreateResourceRequest("GET", url);
+  request.mode = mojom::RequestMode::kNoCors;
+  request.request_initiator = initiator;
+  EXPECT_EQ(net::ERR_FAILED, LoadRequest(request));
+
+  // Socket should have been destroyed.
+  EXPECT_FALSE(socket_data.socket());
+}
+
 }  // namespace network