CVE-2026-113211
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/service_worker/service_worker_container_host.cc |
modified | |
TEST_Fcontent/browser/service_worker/service_worker_container_host_unittest.cc |
modified |
Files Changed
content/browser/service_worker/service_worker_container_host.cccontent/browser/service_worker/service_worker_test_utils.cccontent/browser/service_worker/service_worker_client.cccontent/browser/service_worker/service_worker_consts.hcontent/browser/service_worker/service_worker_test_utils.hcontent/browser/service_worker/service_worker_container_host_unittest.cccontent/browser/service_worker/service_worker_container_host.h
Patch
From f0b22b07220fdc1b37af4eb0443bfed2bf90c0aa Mon Sep 17 00:00:00 2001 From: Takashi Nakayama <[email protected]> Date: Thu, 17 Sep 2026 23:17:55 -0700 Subject: [PATCH] ServiceWorker: Reject ServiceWorkerContainerHost requests from sandboxed clients ServiceWorkerContainerHostForClient already stores the client's PolicyContainerPolicies (including sandbox_flags) at commit time, but previously validated incoming requests against URL-derived origins only. A client sandboxed without allow-same-origin has an opaque origin and is denied access to navigator.serviceWorker in the renderer, so these messages should never arrive from a well-behaved renderer. Add an IsSandboxedWithoutAllowSameOrigin() helper that consults policy_container_policies_.sandbox_flags and check it alongside the existing IsClientValidForCall() gate across all mojo handlers of ServiceWorkerContainerHostForClient. Also plumb PolicyContainerPolicies through CommittedServiceWorkerClient so unit tests can exercise these paths. Bug: 498828493 Change-Id: I8c4cb3b1c00762e44eaec53ff900b0cf90bfb102 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8410150 Reviewed-by: Shunya Shishido <[email protected]> Reviewed-by: Yoshisato Yanagisawa <[email protected]> Commit-Queue: Takashi Nakayama <[email protected]> Cr-Commit-Position: refs/heads/main@{#1700511} --- diff --git a/content/browser/service_worker/service_worker_client.cc b/content/browser/service_worker/service_worker_client.cc index 2f4a155..255d06e 100644 --- a/content/browser/service_worker/service_worker_client.cc +++ b/content/browser/service_worker/service_worker_client.cc @@ -780,6 +780,10 @@ return false; } + if (container_host_ && !container_host_->HasValidSandboxFlags()) { + return false; + } + if (!url_.is_valid()) { return false; } diff --git a/content/browser/service_worker/service_worker_consts.h b/content/browser/service_worker/service_worker_consts.h index 566758a..d2328385 100644 --- a/content/browser/service_worker/service_worker_consts.h +++ b/content/browser/service_worker/service_worker_consts.h @@ -12,6 +12,10 @@ namespace content { struct ServiceWorkerConsts { + static constexpr char kBadMessageFromSandboxedClient[] = + "The request message should not come from a sandboxed client without " + "allow-same-origin."; + static constexpr char kBadMessageFromUnsupportedClient[] = "The request message should not come from an unsupported client."; diff --git a/content/browser/service_worker/service_worker_container_host.cc b/content/browser/service_worker/service_worker_container_host.cc index 695f7a3..d42fef81 100644 --- a/content/browser/service_worker/service_worker_container_host.cc +++ b/content/browser/service_worker/service_worker_container_host.cc @@ -33,6 +33,7 @@ #include "ipc/constants.mojom.h" #include "mojo/public/cpp/bindings/callback_helpers.h" #include "service_worker_container_host.h" +#include "services/network/public/mojom/web_sandbox_flags.mojom-shared.h" #include "third_party/blink/public/common/features.h" #include "third_party/blink/public/common/service_worker/embedded_worker_status.h" #include "third_party/blink/public/common/service_worker/service_worker_status_code.h" @@ -131,6 +132,21 @@ RegisterCallback callback) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (!IsClientValidForCall(service_worker_client())) { + mojo::ReportBadMessage( + ServiceWorkerConsts::kBadMessageFromUnsupportedClient); + std::move(callback).Run(blink::mojom::ServiceWorkerErrorType::kUnknown, + std::string(), nullptr); + return; + } + + if (!HasValidSandboxFlags()) { + mojo::ReportBadMessage(ServiceWorkerConsts::kBadMessageFromSandboxedClient); + std::move(callback).Run(blink::mojom::ServiceWorkerErrorType::kUnknown, + std::string(), nullptr); + return; + } + if (!CanServeContainerHostMethods( &callback, options->scope, script_url, base::StringPrintf( @@ -141,14 +157,6 @@ return; } - if (!IsClientValidForCall(service_worker_client())) { - mojo::ReportBadMessage( - ServiceWorkerConsts::kBadMessageFromUnsupportedClient); - std::move(callback).Run(blink::mojom::ServiceWorkerErrorType::kUnknown, - std::string(), nullptr); - return; - } - std::vector<GURL> urls = {url_for_access_check(), options->scope, script_url}; if (!service_worker_security_utils::AllOriginsMatchAndCanAccessServiceWorkers( urls)) { @@ -326,6 +334,11 @@ blink::mojom::ControllerServiceWorkerPurpose purpose) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (!HasValidSandboxFlags()) { + mojo::ReportBadMessage(ServiceWorkerConsts::kBadMessageFromSandboxedClient); + return; + } + // TODO(kinuko): Log the reasons we drop the request. if (!context() || !controller()) { return; @@ -344,8 +357,22 @@ additional_receivers_.Add(this, std::move(receiver)); } +void ServiceWorkerContainerHostForClient::CloneContainerHost( + mojo::PendingReceiver<blink::mojom::ServiceWorkerContainerHost> receiver) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (!HasValidSandboxFlags()) { + mojo::ReportBadMessage(ServiceWorkerConsts::kBadMessageFromSandboxedClient); + return; + } + ServiceWorkerContainerHost::CloneContainerHost(std::move(receiver)); +} + void ServiceWorkerContainerHostForClient::HintToUpdateServiceWorker() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (!HasValidSandboxFlags()) { + mojo::ReportBadMessage(ServiceWorkerConsts::kBadMessageFromSandboxedClient); + return; + } service_worker_client().HintToUpdateServiceWorker(); } @@ -353,11 +380,20 @@ const std::vector<base::FilePath>& file_paths, EnsureFileAccessCallback callback) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (!HasValidSandboxFlags()) { + mojo::ReportBadMessage(ServiceWorkerConsts::kBadMessageFromSandboxedClient); + std::move(callback).Run(); + return; + } service_worker_client().EnsureFileAccess(file_paths, std::move(callback)); } void ServiceWorkerContainerHostForClient::OnExecutionReady() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (!HasValidSandboxFlags()) { + mojo::ReportBadMessage(ServiceWorkerConsts::kBadMessageFromSandboxedClient); + return; + } service_worker_client().OnExecutionReady(); } @@ -1105,6 +1141,13 @@ std::nullopt, std::move(object_infos)); } +bool ServiceWorkerContainerHostForClient::HasValidSandboxFlags() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + return (policy_container_policies_.sandbox_flags & + network::mojom::WebSandboxFlags::kOrigin) != + network::mojom::WebSandboxFlags::kOrigin; +} + bool ServiceWorkerContainerHostForClient::IsValidGetRegistrationMessage( const GURL& client_url, std::string* out_error) const { @@ -1113,6 +1156,10 @@ *out_error = ServiceWorkerConsts::kBadMessageFromUnsupportedClient; return false; } + if (!HasValidSandboxFlags()) { + *out_error = ServiceWorkerConsts::kBadMessageFromSandboxedClient; + return false; + } if (!client_url.is_valid()) { *out_error = ServiceWorkerConsts::kBadMessageInvalidURL; return false; @@ -1134,6 +1181,10 @@ *out_error = ServiceWorkerConsts::kBadMessageFromUnsupportedClient; return false; } + if (!HasValidSandboxFlags()) { + *out_error = ServiceWorkerConsts::kBadMessageFromSandboxedClient; + return false; + } if (!OriginCanAccessServiceWorkers(url_for_access_check())) { *out_error = ServiceWorkerConsts::kBadMessageImproperOrigins; return false; @@ -1149,6 +1200,10 @@ *out_error = ServiceWorkerConsts::kBadMessageFromUnsupportedClient; return false; } + if (!HasValidSandboxFlags()) { + *out_error = ServiceWorkerConsts::kBadMessageFromSandboxedClient; + return false; + } if (get_ready_callback_) { *out_error = @@ -1415,6 +1470,13 @@ scoped_refptr<ServiceWorkerVersion> version, ::blink::TransferableMessage message, StatusCallback callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (!HasValidSandboxFlags()) { + mojo::ReportBadMessage(ServiceWorkerConsts::kBadMessageFromSandboxedClient); + std::move(callback).Run(blink::ServiceWorkerStatusCode::kErrorDisallowed); + return; + } + bool is_allowed = AllowServiceWorker(version->scope(), version->script_url()); ServiceWorkerMetrics::RecordMessageDispatchContextValidationResult( is_allowed @@ -1446,6 +1508,13 @@ outside_fetch_client_settings_object, blink::mojom::ServiceWorkerRegistrationObjectHost::UpdateCallback callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (!HasValidSandboxFlags()) { + mojo::ReportBadMessage(ServiceWorkerConsts::kBadMessageFromSandboxedClient); + std::move(callback).Run(blink::mojom::ServiceWorkerErrorType::kUnknown, + std::string()); + return; + } // Don't delay update() if called by non-ServiceWorkers. registration->ExecuteUpdate(std::move(outside_fetch_client_settings_object), std::move(callback)); diff --git a/content/browser/service_worker/service_worker_container_host.h b/content/browser/service_worker/service_worker_container_host.h index c75399d..16120a47 100644 --- a/content/browser/service_worker/service_worker_container_host.h +++ b/content/browser/service_worker/service_worker_container_host.h @@ -307,11 +307,19 @@ void EnsureControllerServiceWorker( mojo::PendingReceiver<blink::mojom::ControllerServiceWorker> receiver, blink::mojom::ControllerServiceWorkerPurpose purpose) override; + void CloneContainerHost( + mojo::PendingReceiver<blink::mojom::ServiceWorkerContainerHost> receiver) + override; void HintToUpdateServiceWorker() override; void EnsureFileAccess(const std::vector<base::FilePath>& file_paths, EnsureFileAccessCallback callback) override; void OnExecutionReady() override; + // Returns true if the client's sandbox flags allow access to a + // ServiceWorkerContainer (i.e. not sandboxed without the allow-same-origin + // token). + bool HasValidSandboxFlags() const; + // Implements ServiceWorkerContainerHost. const base::WeakPtr<ServiceWorkerContextCore>& context() const override; base::WeakPtr<ServiceWorkerContainerHost> AsWeakPtr() override; diff --git a/content/browser/service_worker/service_worker_test_utils.cc b/content/browser/service_worker/service_worker_test_utils.cc index eec696e..2cf550c 100644 --- a/content/browser/service_worker/service_worker_test_utils.cc +++ b/content/browser/service_worker/service_worker_test_utils.cc @@ -258,7 +258,8 @@ CommittedServiceWorkerClient::CommittedServiceWorkerClient( ScopedServiceWorkerClient service_worker_client, - const GlobalRenderFrameHostId& render_frame_host_id) + const GlobalRenderFrameHostId& render_frame_host_id, + const PolicyContainerPolicies& policy_container_policies) : service_worker_client_(std::move(service_worker_client.AsWeakPtr())) { // Establish dummy connections to allow sending messages without errors. mojo::PendingRemote<network::mojom::CrossOriginEmbedderPolicyReporter> @@ -273,7 +274,7 @@ auto [container_info, controller_info] = std::move(service_worker_client) .CommitResponseAndRelease( - render_frame_host_id, PolicyContainerPolicies(), + render_frame_host_id, policy_container_policies, std::move(coep_reporter), std::move(dip_reporter), ukm::kInvalidSourceId); @@ -331,13 +332,14 @@ } CommittedServiceWorkerClient::CommittedServiceWorkerClient( - ScopedServiceWorkerClient service_worker_client) + ScopedServiceWorkerClient service_worker_client, + const PolicyContainerPolicies& policy_container_policies) : service_worker_client_(std::move(service_worker_client.AsWeakPtr())) { // For worker cases the mojo call is not emulated (just not implemented). auto [received_info, controller_info] = std::move(service_worker_client) .CommitResponseAndRelease( - /*render_frame_host_id=*/std::nullopt, PolicyContainerPolicies(), + /*rfh_id=*/std::nullopt, policy_container_policies, /*coep_reporter=*/{}, /*dip_reporter=*/{}, ukm::kInvalidSourceId); service_worker_client_->SetContainerReady(); diff --git a/content/browser/service_worker/service_worker_test_utils.h b/content/browser/service_worker/service_worker_test_utils.h index 6d16ac5c..89d4b36 100644 --- a/content/browser/service_worker/service_worker_test_utils.h +++ b/content/browser/service_worker/service_worker_test_utils.h @@ -15,6 +15,7 @@ #include "base/memory/weak_ptr.h" #include "base/run_loop.h" #include "components/services/storage/public/mojom/service_worker_storage_control.mojom.h" +#include "content/browser/renderer_host/policy_container_host.h" #include "content/browser/service_worker/service_worker_cache_writer.h" #include "content/browser/service_worker/service_worker_host.h" #include "content/browser/service_worker/service_worker_single_script_update_checker.h" @@ -78,11 +79,15 @@ // client and takes the keep-aliveness of `ServiceWorkerClient`. CommittedServiceWorkerClient( ScopedServiceWorkerClient service_worker_client, - const GlobalRenderFrameHostId& render_frame_host_id); + const GlobalRenderFrameHostId& render_frame_host_id, + const PolicyContainerPolicies& policy_container_policies = + PolicyContainerPolicies()); // For Worker client. explicit CommittedServiceWorkerClient( - ScopedServiceWorkerClient service_worker_client); + ScopedServiceWorkerClient service_worker_client, + const PolicyContainerPolicies& policy_container_policies = + PolicyContainerPolicies()); CommittedServiceWorkerClient(CommittedServiceWorkerClient&& other); CommittedServiceWorkerClient& operator=( diff --git a/content/browser/service_worker/service_worker_container_host_unittest.cc b/content/browser/service_worker/service_worker_container_host_unittest.cc index 07722f30..5231dd3 100644 --- a/content/browser/service_worker/service_worker_container_host_unittest.cc +++ b/content/browser/service_worker/service_worker_container_host_unittest.cc @@ -46,6 +46,7 @@ #include "mojo/public/cpp/system/functions.h" #include "net/cookies/site_for_cookies.h" #include "services/network/public/cpp/is_potentially_trustworthy.h" +#include "services/network/public/mojom/web_sandbox_flags.mojom-shared.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/common/storage_key/storage_key.h" #include "third_party/blink/public/common/tokens/tokens.h" @@ -172,11 +173,13 @@ // Creates a committed ServiceWorkerContainerHost. CommittedServiceWorkerClient PrepareServiceWorkerContainerHost( - const GURL& document_url) { + const GURL& document_url, + const PolicyContainerPolicies& policies = PolicyContainerPolicies()) { return CommittedServiceWorkerClient( CreateServiceWorkerClient(context_.get(), document_url), GlobalRenderFrameHostId(helper_->mock_render_process_id(), - /*mock frame_routing_id=*/1)); + /*mock frame_routing_id=*/1), + policies); } CommittedServiceWorkerClient FinishNavigation( @@ -260,6 +263,18 @@ return error; } + void GetRegistrationForReady( + blink::mojom::ServiceWorkerContainerHost* container_host) { + base::RunLoop run_loop; + container_host->GetRegistrationForReady(base::BindOnce( + [](base::OnceClosure quit_closure, + blink::mojom::ServiceWorkerRegistrationObjectInfoPtr info) { + std::move(quit_closure).Run(); + }, + run_loop.QuitClosure())); + run_loop.Run(); + } + void OnMojoError(const std::string& error) { bad_messages_.push_back(error); } bool CanFindServiceWorkerClient(ServiceWorkerClient* service_worker_client) { @@ -995,6 +1010,163 @@ EXPECT_EQ(6u, bad_messages_.size()); } +TEST_F(ServiceWorkerContainerHostTest, Register_SandboxedDocumentRejected) { + PolicyContainerPolicies policies; + policies.sandbox_flags = network::mojom::WebSandboxFlags::kOrigin; + CommittedServiceWorkerClient service_worker_client = + PrepareServiceWorkerContainerHost(GURL("https://www.example.com/foo"), + policies); + + EXPECT_TRUE(bad_messages_.empty()); + Register(service_worker_client.host_remote().get(), + GURL("https://www.example.com/"), + GURL("https://www.example.com/sw.js")); + EXPECT_EQ(1u, bad_messages_.size()); +} + +TEST_F(ServiceWorkerContainerHostTest, + Register_SandboxedDocumentWithAllowSameOrigin) { + PolicyContainerPolicies policies; ... (file diff truncated)
Regression Test / PoC
diff --git a/content/browser/service_worker/service_worker_container_host_unittest.cc b/content/browser/service_worker/service_worker_container_host_unittest.cc
index 07722f30..5231dd3 100644
--- a/content/browser/service_worker/service_worker_container_host_unittest.cc
+++ b/content/browser/service_worker/service_worker_container_host_unittest.cc
@@ -46,6 +46,7 @@
#include "mojo/public/cpp/system/functions.h"
#include "net/cookies/site_for_cookies.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
+#include "services/network/public/mojom/web_sandbox_flags.mojom-shared.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/common/tokens/tokens.h"
@@ -172,11 +173,13 @@
// Creates a committed ServiceWorkerContainerHost.
CommittedServiceWorkerClient PrepareServiceWorkerContainerHost(
- const GURL& document_url) {
+ const GURL& document_url,
+ const PolicyContainerPolicies& policies = PolicyContainerPolicies()) {
return CommittedServiceWorkerClient(
CreateServiceWorkerClient(context_.get(), document_url),
GlobalRenderFrameHostId(helper_->mock_render_process_id(),
- /*mock frame_routing_id=*/1));
+ /*mock frame_routing_id=*/1),
+ policies);
}
CommittedServiceWorkerClient FinishNavigation(
@@ -260,6 +263,18 @@
return error;
}
+ void GetRegistrationForReady(
+ blink::mojom::ServiceWorkerContainerHost* container_host) {
+ base::RunLoop run_loop;
+ container_host->GetRegistrationForReady(base::BindOnce(
+ [](base::OnceClosure quit_closure,
+ blink::mojom::ServiceWorkerRegistrationObjectInfoPtr info) {
+ std::move(quit_closure).Run();
+ },
+ run_loop.QuitClosure()));
+ run_loop.Run();
+ }
+
void OnMojoError(const std::string& error) { bad_messages_.push_back(error); }
bool CanFindServiceWorkerClient(ServiceWorkerClient* service_worker_client) {
@@ -995,6 +1010,163 @@
EXPECT_EQ(6u, bad_messages_.size());
}
+TEST_F(ServiceWorkerContainerHostTest, Register_SandboxedDocumentRejected) {
+ PolicyContainerPolicies policies;
+ policies.sandbox_flags = network::mojom::WebSandboxFlags::kOrigin;
+ CommittedServiceWorkerClient service_worker_client =
+ PrepareServiceWorkerContainerHost(GURL("https://www.example.com/foo"),
+ policies);
+
+ EXPECT_TRUE(bad_messages_.empty());
+ Register(service_worker_client.host_remote().get(),
+ GURL("https://www.example.com/"),
+ GURL("https://www.example.com/sw.js"));
+ EXPECT_EQ(1u, bad_messages_.size());
+}
+
+TEST_F(ServiceWorkerContainerHostTest,
+ Register_SandboxedDocumentWithAllowSameOrigin) {
+ PolicyContainerPolicies policies;
+ policies.sandbox_flags = network::mojom::WebSandboxFlags::kAutomaticFeatures;
+ CommittedServiceWorkerClient service_worker_client =
+ PrepareServiceWorkerContainerHost(GURL("https://www.example.com/foo"),
+ policies);
+
+ EXPECT_TRUE(bad_messages_.empty());
+ Register(service_worker_client.host_remote().get(),
+ GURL("https://www.example.com/"),
+ GURL("https://www.example.com/sw.js"));
+ EXPECT_TRUE(bad_messages_.empty());
+}
+
+TEST_F(ServiceWorkerContainerHostTest,
+ GetRegistration_SandboxedDocumentRejected) {
+ PolicyContainerPolicies policies;
+ policies.sandbox_flags = network::mojom::WebSandboxFlags::kOrigin;
+ CommittedServiceWorkerClient service_worker_client =
+ PrepareServiceWorkerContainerHost(GURL("https://www.example.com/foo"),
+ policies);
+
+ EXPECT_TRUE(bad_messages_.empty());
+ GetRegistration(service_worker_client.host_remote().get(),
+ GURL("https://www.example.com/"));
+ EXPECT_EQ(1u, bad_messages_.size());
+}
+
+TEST_F(ServiceWorkerContainerHostTest,
+ GetRegistrations_SandboxedDocumentRejected) {
+ PolicyContainerPolicies policies;
+ policies.sandbox_flags = network::mojom::WebSandboxFlags::kOrigin;
+ CommittedServiceWorkerClient service_worker_client =
+ PrepareServiceWorkerContainerHost(GURL("https://www.example.com/foo"),
+ policies);
+
+ EXPECT_TRUE(bad_messages_.empty());
+ GetRegistrations(service_worker_client.host_remote().get());
+ EXPECT_EQ(1u, bad_messages_.size());
+}
+
+TEST_F(ServiceWorkerContainerHostTest,
+ GetRegistrationForReady_SandboxedDocumentRejected) {
+ PolicyContainerPolicies policies;
+ policies.sandbox_flags = network::mojom::WebSandboxFlags::kOrigin;
+ CommittedServiceWorkerClient service_worker_client =
+ PrepareServiceWorkerContainerHost(GURL("https://www.example.com/foo"),
+ policies);
+
+ EXPECT_TRUE(bad_messages_.empty());
+ GetRegistrationForReady(service_worker_client.host_remote().get());
+ EXPECT_EQ(1u, bad_messages_.size());
+}
+
+TEST_F(ServiceWorkerContainerHostTest,
+ OnExecutionReady_SandboxedDocumentRejected) {
+ PolicyContainerPolicies policies;
+ policies.sandbox_flags = network::mojom::WebSandboxFlags::kOrigin;
+ CommittedServiceWorkerClient service_worker_client =
+ PrepareServiceWorkerContainerHost(GURL("https://www.example.com/foo"),
+ policies);
+
+ EXPECT_TRUE(bad_messages_.empty());
+ service_worker_client.host_remote()->OnExecutionReady();
+ service_worker_client.host_remote().FlushForTesting();
+ EXPECT_EQ(1u, bad_messages_.size());
+}
+
+TEST_F(ServiceWorkerContainerHostTest,
+ HintToUpdateServiceWorker_SandboxedDocumentRejected) {
+ PolicyContainerPolicies policies;
+ policies.sandbox_flags = network::mojom::WebSandboxFlags::kOrigin;
+ CommittedServiceWorkerClient service_worker_client =
+ PrepareServiceWorkerContainerHost(GURL("https://www.example.com/foo"),
+ policies);
+
+ EXPECT_TRUE(bad_messages_.empty());
+ service_worker_client.host_remote()->HintToUpdateServiceWorker();
+ service_worker_client.host_remote().FlushForTesting();
+ EXPECT_EQ(1u, bad_messages_.size());
+}
+
+TEST_F(ServiceWorkerContainerHostTest,
+ EnsureControllerServiceWorker_SandboxedDocumentRejected) {
+ PolicyContainerPolicies policies;
+ policies.sandbox_flags = network::mojom::WebSandboxFlags::kOrigin;
+ CommittedServiceWorkerClient service_worker_client =
+ PrepareServiceWorkerContainerHost(GURL("https://www.example.com/foo"),
+ policies);
+
+ EXPECT_TRUE(bad_messages_.empty());
+ mojo::PendingRemote<blink::mojom::ControllerServiceWorker> remote;
+ service_worker_client.host_remote()->EnsureControllerServiceWorker(
+ remote.InitWithNewPipeAndPassReceiver(),
+ blink::mojom::ControllerServiceWorkerPurpose::FETCH_SUB_RESOURCE);
+ service_worker_client.host_remote().FlushForTesting();
+ EXPECT_EQ(1u, bad_messages_.size());
+}
+
+TEST_F(ServiceWorkerContainerHostTest,
+ EnsureFileAccess_SandboxedDocumentRejected) {
+ PolicyContainerPolicies policies;
+ policies.sandbox_flags = network::mojom::WebSandboxFlags::kOrigin;
+ CommittedServiceWorkerClient service_worker_client =
+ PrepareServiceWorkerContainerHost(GURL("https://www.example.com/foo"),
+ policies);
+
+ EXPECT_TRUE(bad_messages_.empty());
+ base::RunLoop run_loop;
+ service_worker_client.host_remote()->EnsureFileAccess({},
+ run_loop.QuitClosure());
+ run_loop.Run();
+ EXPECT_EQ(1u, bad_messages_.size());
+}
+
+TEST_F(ServiceWorkerContainerHostTest,
+ CloneContainerHost_SandboxedDocumentRejected) {
+ PolicyContainerPolicies policies;
+ policies.sandbox_flags = network::mojom::WebSandboxFlags::kOrigin;
+ CommittedServiceWorkerClient service_worker_client =
+ PrepareServiceWorkerContainerHost(GURL("https://www.example.com/foo"),
+ policies);
+
+ EXPECT_TRUE(bad_messages_.empty());
+ mojo::PendingRemote<blink::mojom::ServiceWorkerContainerHost> cloned_remote;
+ service_worker_client.host_remote()->CloneContainerHost(
+ cloned_remote.InitWithNewPipeAndPassReceiver());
+ service_worker_client.host_remote().FlushForTesting();
+ EXPECT_EQ(1u, bad_messages_.size());
+}
+
+TEST_F(ServiceWorkerContainerHostTest,
+ IsEligibleForServiceWorkerController_SandboxedDocumentFalse) {
+ PolicyContainerPolicies policies;
+ policies.sandbox_flags = network::mojom::WebSandboxFlags::kOrigin;
+ CommittedServiceWorkerClient service_worker_client =
+ PrepareServiceWorkerContainerHost(GURL("https://www.example.com/foo"),
+ policies);
+
+ EXPECT_FALSE(service_worker_client->IsEligibleForServiceWorkerController());
+}
+
TEST_F(ServiceWorkerContainerHostTest, Register_BadCharactersShouldFail) {
CommittedServiceWorkerClient service_worker_client =
PrepareServiceWorkerContainerHost(GURL("https://www.example.com"));
Original Bug Report
Site Isolation bypass via Service Worker registration from sandboxed iframes
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.
Overview: A logic flaw in the browser’s Service Worker IPC handler potentially allows a compromised, CSP-sandboxed renderer to register a Service Worker for its precursor origin. The registration process relies on a StorageKey derived from the navigation URL rather than the document’s true opaque origin. This bypasses Site Isolation and CSP restrictions, granting an attacker persistent control over the precursor origin.
Affected files:
content/browser/service_worker/service_worker_container_host.cccontent/browser/service_worker/service_worker_client.cccontent/browser/service_worker/service_worker_context_core.cc
Estimated timestamp from git blame: 2025-02-07
Description
There is a potential Site Isolation and CSP sandbox bypass in the Service Worker registration flow. When a document is loaded in an iframe with Content-Security-Policy: sandbox (lacking allow-same-origin), it is assigned an opaque origin. However, the browser-side Mojo IPC handler for Service Worker registration fails to account for this opacity, allowing a compromised sandboxed renderer to register a Service Worker for the non-opaque precursor origin.
The root cause lies in how ServiceWorkerContainerHostForClient::Register (in content/browser/service_worker/service_worker_container_host.cc) validates the registration request:
- It performs URL matching using
url_for_access_check()andservice_worker_security_utils::AllOriginsMatchAndCanAccessServiceWorkers. The latter compares origins usingDeprecatedGetOriginAsURL(). This strictly derives the origin from the URL string, ignoring the frame’s opaque sandboxed state. - It retrieves the registration key via
service_worker_client().key(). ThisStorageKeyis initialized early during the request phase viaServiceWorkerClient::CalculateStorageKeyForUpdateUrls. Because it is initialized usingurl::Origin::Create(url)before the final sandbox flags are applied at commit, it results in a non-opaqueStorageKeythat matches the precursor origin. - The final validation step in
ServiceWorkerContextCore::IsValidRegisterRequestexplicitly checksif (key.origin().opaque()), but this check is bypassed because the erroneously constructedStorageKeyis not opaque.
Furthermore, there are no ChildProcessSecurityPolicy origin access checks applied during the Register IPC handling that would prevent the isolated, sandboxed renderer process from acting on behalf of the precursor origin.
Potential Exploitation Steps
(Note: Our tooling agent does not yet have the ability to run code to provide a working PoC, so these are suggested steps based on static analysis.)
- An attacker targets a site (
https://site.example) that embeds untrusted content inside an iframe protected byContent-Security-Policy: sandbox(withoutallow-same-origin). - The attacker gains Remote Code Execution (RCE) within the isolated renderer process hosting this sandboxed iframe.
- Bypassing Blink’s standard JS API protections, the compromised renderer directly sends a
blink::mojom::ServiceWorkerContainerHost::RegisterMojo message to the browser process, specifying ascript_urlofhttps://site.example/sw.jsand ascopeofhttps://site.example/. - The browser process validates the request using the URL-derived
StorageKeyrather than the true opaque origin, passing all security checks. - The Service Worker is successfully registered. The attacker can now intercept and modify all future network traffic for the precursor origin
https://site.example.
Suggested Fix
Ensure that the StorageKey held by the ServiceWorkerClient is correctly updated during navigation commit to reflect the frame’s true opaque origin when CSP sandbox flags apply.
Alternatively, explicitly enforce policy_container_policies_.sandbox_flags within ServiceWorkerContainerHostForClient::Register, or verify that the calling renderer process has the right to access the precursor origin via ChildProcessSecurityPolicy::GetInstance()->CanAccessOrigin() before proceeding with the registration.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.