CVE-2026-79015
Overview
Background
- `OnReportConsoleMessage`
- The browser-process handler in
ServiceWorkerContextCorethat receives console-message reports emitted by a service worker running in a renderer. - `source_url`
- A renderer-supplied parameter naming the script URL a console message came from, which web content can freely set via the V8
//# sourceURL=pragma. - `IsBuiltinComponent` / `HasWebUIScheme`
- Trust checks that treat privileged (e.g.
chrome://, WebUI) origins as built-in components, granting them relaxed logging behavior including bypass of Incognito logging restrictions and higher log severity. - `ServiceWorkerVersion::script_url()` / `key().origin()`
- The browser-authenticated script URL and storage-key origin of the actual service-worker version, which cannot be spoofed by the renderer.
Root Cause Analysis
ServiceWorkerContextCore::OnReportConsoleMessage computed is_builtin_component by passing the renderer-provided source_url to HasWebUIScheme(source_url) and IsBuiltinComponent(browser_context, url::Origin::Create(source_url)). Because ordinary web content can set source_url to an arbitrary string (via the //# sourceURL= pragma), an unprivileged service worker could report a message with a spoofed chrome:// URL and be misclassified as a built-in/privileged component. This violated the invariant that a privileged-component determination must be based on the browser’s authenticated view of the worker, not on renderer-controlled data.
The fix evaluates the check against version->script_url() and version->key().origin(), which the browser process derives from the trusted registration, so a spoofed source_url no longer influences the trust decision. The shipped regression test confirms that a chrome://settings/ source_url causes IsBuiltinComponent to be queried with the worker’s real https://www.example.com origin.
source_url input; the fix substitutes the browser-authenticated version->script_url() and version->key().origin() so the classification can no longer be spoofed.Attack Path
- Register an ordinary service worker
An attacker on unprivileged web content registers a service worker under their own origin (e.g.
https://www.example.com). - Spoof the source URL
Inside the worker the attacker emits a console message whose
source_urlis forced to a privileged value such aschrome://settings/using the//# sourceURL=pragma. - Trigger the trust check
OnReportConsoleMessagerunsHasWebUIScheme/IsBuiltinComponentagainst the spoofed URL and setsis_builtin_component = true. - Escalate logging The message is then logged as if from a built-in component, bypassing Incognito logging restrictions and escalating log severity as described in the commit message.
Impact Assessment
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
BuiltinComponentTestBrowserClientcontent/browser/service_worker/service_worker_context_core_unittest.cc |
modified | |
TEST_Fcontent/browser/service_worker/service_worker_context_core_unittest.cc |
modified |
Files Changed
content/browser/service_worker/service_worker_context_core.cccontent/browser/service_worker/service_worker_context_core_unittest.cc
Audit Directions
- Renderer-supplied URLs in trust checksGrep for security or privilege decisions that consume
source_url, script names, or other IPC string parameters viaurl::Origin::Create/HasWebUISchemeinstead of an authenticated origin. - `//# sourceURL=` influenceLook for browser-side logging, DevTools, or reporting paths that key behavior on the script/source URL a renderer attaches to console or error events.
- Origin vs. reported-URL mismatchAudit service-worker and worker IPC handlers where the acting principal (
version/StorageKey) is available but the code trusts a separately-transmitted URL parameter.
Patch
From d1967cd13713c3104c7751ebb2d536867cfb7385 Mon Sep 17 00:00:00 2001 From: Andrea Orru <[email protected]> Date: Tue, 14 Jul 2026 18:42:34 -0700 Subject: [PATCH] [Service Workers] Use version origin for IsBuiltinComponent check ServiceWorkerContextCore::OnReportConsoleMessage calculated is_builtin_component using the renderer-provided source_url parameter. Because standard web content can set source_url via the V8 //# sourceURL= pragma, an unprivileged service worker could spoof a chrome:// URL to bypass Incognito logging restrictions and escalate log severity. Fix this by evaluating is_builtin_component against the ServiceWorker version's authenticated script_url() and key().origin() instead of source_url. Bug: 522291712 Change-Id: I224739dd6b4aa509925c187ed1f83ec92af12d39 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8088995 Reviewed-by: Yoshisato Yanagisawa <[email protected]> Commit-Queue: Andrea Orru <[email protected]> Cr-Commit-Position: refs/heads/main@{#1662317} --- diff --git a/content/browser/service_worker/service_worker_context_core.cc b/content/browser/service_worker/service_worker_context_core.cc index d6bfd3c0..154e32f8 100644 --- a/content/browser/service_worker/service_worker_context_core.cc +++ b/content/browser/service_worker/service_worker_context_core.cc @@ -1385,9 +1385,9 @@ DCHECK(browser_context); DCHECK_EQ(this, version->context().get()); const bool is_builtin_component = - HasWebUIScheme(source_url) || + HasWebUIScheme(version->script_url()) || GetContentClient()->browser()->IsBuiltinComponent( - browser_context, url::Origin::Create(source_url)); + browser_context, version->key().origin()); LogConsoleMessage(message_level, message, line_number, is_builtin_component, wrapper_->is_incognito(), 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 d92a3e9..1059fa9 100644 --- a/content/browser/service_worker/service_worker_context_core_unittest.cc +++ b/content/browser/service_worker/service_worker_context_core_unittest.cc @@ -15,7 +15,9 @@ #include "content/browser/service_worker/service_worker_registration.h" #include "content/browser/service_worker/service_worker_test_utils.h" #include "content/browser/service_worker/service_worker_version.h" +#include "content/public/common/content_client.h" #include "content/public/test/browser_task_environment.h" +#include "content/public/test/test_content_browser_client.h" #include "content/public/test/test_utils.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/common/storage_key/storage_key.h" @@ -373,4 +375,58 @@ EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed, status); } +// Test browser client to capture the origin checked by `IsBuiltinComponent()`. +class BuiltinComponentTestBrowserClient : public TestContentBrowserClient { + public: + bool IsBuiltinComponent(BrowserContext* browser_context, + const url::Origin& origin) override { + last_queried_origin_ = origin; + return origin.scheme() == "chrome"; + } + + const std::optional<url::Origin>& last_queried_origin() const { + return last_queried_origin_; + } + + private: + std::optional<url::Origin> last_queried_origin_; +}; + +// Ensures that `OnReportConsoleMessage` checks whether the service worker's +// authenticated origin (rather than a renderer-provided `source_url` which can +// be spoofed) is a built-in component. Regression test for crbug.com/522291712. +TEST_F(ServiceWorkerContextCoreTest, OnReportConsoleMessageUsesVersionOrigin) { + BuiltinComponentTestBrowserClient test_browser_client; + ContentBrowserClient* old_browser_client = + SetBrowserClientForTesting(&test_browser_client); + base::ScopedClosureRunner reset_browser_client(base::BindOnce( + [](ContentBrowserClient* client) { SetBrowserClientForTesting(client); }, + old_browser_client)); + + const GURL script("https://www.example.com/sw.js"); + const GURL scope("https://www.example.com/"); + const url::Origin origin = url::Origin::Create(scope); + const blink::StorageKey key = blink::StorageKey::CreateFirstParty(origin); + + blink::mojom::ServiceWorkerRegistrationOptions options; + options.scope = scope; + scoped_refptr<ServiceWorkerRegistration> registration; + RegisterServiceWorker(script, key, options, ®istration); + ASSERT_TRUE(registration->active_version()); + + // Report a console message with a spoofed source_url (e.g. + // chrome://settings/). + const GURL spoofed_source_url("chrome://settings/"); + context()->OnReportConsoleMessage( + registration->active_version(), + blink::mojom::ConsoleMessageSource::kConsoleApi, + blink::mojom::ConsoleMessageLevel::kError, u"spoofed console message", 1, + spoofed_source_url); + + // Verify that IsBuiltinComponent was called with the Service Worker's actual + // origin rather than the spoofed source_url origin. + ASSERT_TRUE(test_browser_client.last_queried_origin().has_value()); + EXPECT_EQ(origin, test_browser_client.last_queried_origin().value()); +} + } // namespace content
Regression Test / PoC
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 d92a3e9..1059fa9 100644
--- a/content/browser/service_worker/service_worker_context_core_unittest.cc
+++ b/content/browser/service_worker/service_worker_context_core_unittest.cc
@@ -15,7 +15,9 @@
#include "content/browser/service_worker/service_worker_registration.h"
#include "content/browser/service_worker/service_worker_test_utils.h"
#include "content/browser/service_worker/service_worker_version.h"
+#include "content/public/common/content_client.h"
#include "content/public/test/browser_task_environment.h"
+#include "content/public/test/test_content_browser_client.h"
#include "content/public/test/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
@@ -373,4 +375,58 @@
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed, status);
}
+// Test browser client to capture the origin checked by `IsBuiltinComponent()`.
+class BuiltinComponentTestBrowserClient : public TestContentBrowserClient {
+ public:
+ bool IsBuiltinComponent(BrowserContext* browser_context,
+ const url::Origin& origin) override {
+ last_queried_origin_ = origin;
+ return origin.scheme() == "chrome";
+ }
+
+ const std::optional<url::Origin>& last_queried_origin() const {
+ return last_queried_origin_;
+ }
+
+ private:
+ std::optional<url::Origin> last_queried_origin_;
+};
+
+// Ensures that `OnReportConsoleMessage` checks whether the service worker's
+// authenticated origin (rather than a renderer-provided `source_url` which can
+// be spoofed) is a built-in component. Regression test for crbug.com/522291712.
+TEST_F(ServiceWorkerContextCoreTest, OnReportConsoleMessageUsesVersionOrigin) {
+ BuiltinComponentTestBrowserClient test_browser_client;
+ ContentBrowserClient* old_browser_client =
+ SetBrowserClientForTesting(&test_browser_client);
+ base::ScopedClosureRunner reset_browser_client(base::BindOnce(
+ [](ContentBrowserClient* client) { SetBrowserClientForTesting(client); },
+ old_browser_client));
+
+ const GURL script("https://www.example.com/sw.js");
+ const GURL scope("https://www.example.com/");
+ const url::Origin origin = url::Origin::Create(scope);
+ const blink::StorageKey key = blink::StorageKey::CreateFirstParty(origin);
+
+ blink::mojom::ServiceWorkerRegistrationOptions options;
+ options.scope = scope;
+ scoped_refptr<ServiceWorkerRegistration> registration;
+ RegisterServiceWorker(script, key, options, ®istration);
+ ASSERT_TRUE(registration->active_version());
+
+ // Report a console message with a spoofed source_url (e.g.
+ // chrome://settings/).
+ const GURL spoofed_source_url("chrome://settings/");
+ context()->OnReportConsoleMessage(
+ registration->active_version(),
+ blink::mojom::ConsoleMessageSource::kConsoleApi,
+ blink::mojom::ConsoleMessageLevel::kError, u"spoofed console message", 1,
+ spoofed_source_url);
+
+ // Verify that IsBuiltinComponent was called with the Service Worker's actual
+ // origin rather than the spoofed source_url origin.
+ ASSERT_TRUE(test_browser_client.last_queried_origin().has_value());
+ EXPECT_EQ(origin, test_browser_client.last_queried_origin().value());
+}
+
} // namespace content