Medium watch CVSS 8.3 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in ServiceWorker
DescriptionInappropriate implementation in ServiceWorker
ComponentServiceWorker
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker497250399
Fix commitb01d62a11f3b (chromium/src) +0/-48
CWECWE-693
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-28 (per vendor advisory)
Fix landed2026-03-31 (public git)
Disclosed2026-05-05
Patch gap35 days — fix public in source before the advisory

Background

Blink runtime features
process-wide feature flags (blink::WebRuntimeFeatures) that enable or disable web-platform capabilities in a renderer, some of which (e.g. MojomJS) are highly privileged.
`EmbeddedWorkerStartParams.forced_enabled_runtime_features`
a Mojo field carrying a list of runtime-feature names the browser asked the renderer to force-enable when starting a service worker.
`UpdateEnabledBlinkRuntimeFeaturesInIsolatedWorker`
a ContentBrowserClient hook the embedder could implement to populate that list, invoked only when the service worker’s process was locked to a site.
`ProcessLock::IsLockedToSite`
the check that a renderer process is dedicated to one site, previously used as the guard for applying the forced features.

Root Cause Analysis

When starting an embedded (service) worker, embedded_worker_instance.cc called ContentBrowserClient::UpdateEnabledBlinkRuntimeFeaturesInIsolatedWorker to populate params->forced_enabled_runtime_features, and the renderer-side embedded_worker_instance_client_impl.cc then looped over that list calling blink::WebRuntimeFeatures::EnableFeatureFromString(feature, true), force-enabling each named feature process-wide. The mechanism was gated only on rph->GetProcessLock().IsLockedToSite(), and the field was in fact no longer used, but it left a dormant, powerful capability: a list of feature-name strings that, if populated, would flip on highly privileged Blink features inside the worker’s renderer. As the commit states, a compromised service worker could — depending on the future call tree — cause sensitive features such as MojomJS to be enabled, weakening the renderer sandbox’s attack surface.

The fix removes the entire path: the UpdateEnabledBlinkRuntimeFeaturesInIsolatedWorker virtual and its default definition, the browser-side call and its CHECK, the renderer-side enable loop, and the forced_enabled_runtime_features field from the embedded_worker.mojom EmbeddedWorkerStartParams. With no field to carry feature names and no code to apply them, service worker startup can no longer force-enable arbitrary runtime features. This is a capability removal, not a check re-ordering.

Key insight
The single mistake was retaining an unused but powerful browser-to-renderer channel that could force-enable arbitrary, highly privileged Blink runtime features for a service worker; the fix eliminates the field, the hook, and the enable loop entirely so the capability no longer exists.

Attack Path

  1. Compromise a service worker renderer An attacker gains code execution in a renderer running a service worker (the standard threat model for renderer compromise).
  2. Reach the feature-enable channel The startup path fed forced_enabled_runtime_features from the browser into the renderer, which called EnableFeatureFromString for each name, so a value that reaches this list flips on the corresponding Blink runtime feature process-wide.
  3. Enable a sensitive feature Depending on the future call tree, a compromised worker could cause privileged features such as MojomJS to be enabled, expanding the renderer’s capabilities beyond its intended sandbox surface (inferred; the commit describes this as a potential source of a security issue rather than a demonstrated exploit).
  4. Removed by the patch After the fix there is no forced_enabled_runtime_features field and no enable loop, so the escalation channel is closed.

Impact Assessment

The change removes a latent privilege-escalation surface in which a compromised service worker could force-enable highly privileged Blink runtime features (e.g. MojomJS) in its renderer, broadening what a renderer-compromise attacker could reach. The precondition is a compromised/malicious service worker renderer; the field was already unused, so this is defense-in-depth hardening. The advisory classifies it as “Inappropriate implementation in ServiceWorker”; no exploited-in-the-wild status is asserted, and the escalation to enabling sensitive features is described by the commit as potential and is (inferred) rather than demonstrated.

Changed Functions

FunctionChangeNotes
for
content/renderer/service_worker/embedded_worker_instance_client_impl.cc
modified

Files Changed

  • content/browser/service_worker/embedded_worker_instance.cc
  • content/public/browser/content_browser_client.cc
  • content/public/browser/content_browser_client.h
  • content/renderer/service_worker/embedded_worker_instance_client_impl.cc
  • third_party/blink/public/mojom/service_worker/embedded_worker.mojom

Audit Directions

  • Dormant privileged fields
    Audit Mojo structs and ContentBrowserClient hooks for fields/methods that force-enable privileged capabilities but appear unused, since they remain reachable if a future or compromised caller populates them.
  • String-named feature toggles crossing the browser/renderer boundary
    Review any browser-to-renderer path that enables features by name (EnableFeatureFromString-style), verifying the value cannot originate from or be influenced by a compromised renderer.
  • Process-lock as the sole gate
    Examine capabilities guarded only by IsLockedToSite, confirming that site isolation alone is not treated as sufficient authorization to grant highly privileged features.
From b01d62a11f3b07e5cd754949abd04b1d879e56b4 Mon Sep 17 00:00:00 2001
From: Ari Chivukula <arichiv@chromium.org>
Date: Tue, 31 Mar 2026 07:38:24 -0700
Subject: [PATCH] Remove Blink runtime feature force-enable power from ServiceWorkers.

This change removes the
ContentBrowserClient::UpdateEnabledBlinkRuntimeFeaturesInIsolatedWorker
method and the associated forced_enabled_runtime_features field from
EmbeddedWorkerStartParams, which were used to enable specific Blink
runtime features for service workers in process-isolated renderers.

This field was no longer used, and is the potential source of a security
issue as a compromised service worker could enable sensitive features
like MojomJS depending on the future call tree.

Fixed: 497250399
Change-Id: I5a8a00e91bdba42abc9e25196fba5f67fec66250
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7715723
Auto-Submit: Ari Chivukula <arichiv@chromium.org>
Commit-Queue: Ari Chivukula <arichiv@chromium.org>
Reviewed-by: Arthur Sonzogni <arthursonzogni@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1607853}
---

diff --git a/content/browser/service_worker/embedded_worker_instance.cc b/content/browser/service_worker/embedded_worker_instance.cc
index 8269d6f..13dc706 100644
--- a/content/browser/service_worker/embedded_worker_instance.cc
+++ b/content/browser/service_worker/embedded_worker_instance.cc
@@ -394,22 +394,6 @@
         params->devtools_worker_token.ToString());
   }
 
-  // To enable runtime features, the render process must be locked to the site.
-  // These features are highly privileged, so the renderer process with such
-  // features enabled shouldn't be used for other sites.
-  //
-  // WebUI schemes are process isolated already. To isolate other sites, the
-  // embedder can override ContentBrowserClient::ShouldLockProcessToSite().
-  if (rph->GetProcessLock().IsLockedToSite()) {
-    GetContentClient()
-        ->browser()
-        ->UpdateEnabledBlinkRuntimeFeaturesInIsolatedWorker(
-            context_->wrapper()->browser_context(), params->script_url,
-            params->forced_enabled_runtime_features);
-  }
-  CHECK(params->forced_enabled_runtime_features.empty() ||
-        rph->GetProcessLock().IsLockedToSite());
-
   // TODO(crbug.com/40584626): Support changes to blink::RendererPreferences
   // while the worker is running.
   DCHECK(context_->wrapper()->browser_context() ||
diff --git a/content/public/browser/content_browser_client.cc b/content/public/browser/content_browser_client.cc
index 2e640bb..35e4074 100644
--- a/content/public/browser/content_browser_client.cc
+++ b/content/public/browser/content_browser_client.cc
@@ -499,11 +499,6 @@
   return true;
 }
 
-void ContentBrowserClient::UpdateEnabledBlinkRuntimeFeaturesInIsolatedWorker(
-    BrowserContext* context,
-    const GURL& script_url,
-    std::vector<std::string>& out_forced_enabled_runtime_features) {}
-
 bool ContentBrowserClient::AllowSharedWorker(
     const GURL& worker_url,
     const net::SiteForCookies& site_for_cookies,
diff --git a/content/public/browser/content_browser_client.h b/content/public/browser/content_browser_client.h
index ba90f08..27a2f10 100644
--- a/content/public/browser/content_browser_client.h
+++ b/content/public/browser/content_browser_client.h
@@ -934,20 +934,6 @@
       const GURL& scope,
       BrowserContext* browser_context);
 
-  // Allows the embedder to enable process-wide blink features before starting a
-  // service worker. This is similar to
-  // `blink.mojom.CommitNavigationParams.force_enabled_origin_trials` but for
-  // RuntimeFeatures instead of Origin Trials.
-  //
-  // This method is only called when the process that will run the Service
-  // Worker is isolated. These features can be highly privileged, so the
-  // renderer process with such features enabled shouldn't be used for other
-  // sites.
-  virtual void UpdateEnabledBlinkRuntimeFeaturesInIsolatedWorker(
-      BrowserContext* context,
-      const GURL& script_url,
-      std::vector<std::string>& out_forced_enabled_runtime_features);
-
   // Allow the embedder to control if a Shared Worker can be connected from a
   // given tab.
   // This is called on the UI thread.
diff --git a/content/renderer/service_worker/embedded_worker_instance_client_impl.cc b/content/renderer/service_worker/embedded_worker_instance_client_impl.cc
index b4a41b69..09b1643 100644
--- a/content/renderer/service_worker/embedded_worker_instance_client_impl.cc
+++ b/content/renderer/service_worker/embedded_worker_instance_client_impl.cc
@@ -98,10 +98,6 @@
 
   start_data->is_cross_origin_isolated = params->cross_origin_isolated;
 
-  for (const auto& feature : params->forced_enabled_runtime_features) {
-    blink::WebRuntimeFeatures::EnableFeatureFromString(feature, true);
-  }
-
   // `cache_storage` may be null if COEP is not enabled, we cannot bind
   // eagerly in that case.
   mojo::PendingRemote<blink::mojom::CacheStorage> cache_storage =
diff --git a/third_party/blink/public/mojom/service_worker/embedded_worker.mojom b/third_party/blink/public/mojom/service_worker/embedded_worker.mojom
index dfc719e3..68a0ff7 100644
--- a/third_party/blink/public/mojom/service_worker/embedded_worker.mojom
+++ b/third_party/blink/public/mojom/service_worker/embedded_worker.mojom
@@ -91,15 +91,6 @@
   // Used to set up fetch requests.
   RendererPreferences renderer_preferences;
 
-  // List of blink runtime features (based on their names) to enable for this
-  // renderer process. To avoid exposing dangerous features to other sites, this
-  // is only populated if the renderer process is isolated.
-  //
-  // Ideally this would be part of content.mojom.Renderer, but
-  // EmbedderWorkerInstanceClient and content.mojom.Renderer use separate pipes
-  // so a ServiceWorker could be started before the features are enabled.
-  array<string> forced_enabled_runtime_features;
-
   // Used to talk to the service worker from the browser process.
   pending_receiver<ServiceWorker> service_worker_receiver;
 
Loading diff…

Original Bug Report

reported by vm...@google.com

ProtectedMemory bypass for MojoJS via runtime-enabled feature string iteration gadget

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A logic error in the generated code for runtime-enabled features allows an attacker with an arbitrary memory write primitive to bypass ProtectedMemory mitigations for MojoJS. By corrupting the forced_enabled_runtime_features vector during Service Worker startup, an attacker can invoke a generated string-to-setter mapping that legitimately unprotects and enables the flag. This exposes window.Mojo to the web context, bypassing context checks and facilitating a renderer sandbox escape.

Affected files:

  • third_party/blink/renderer/build/scripts/templates/runtime_enabled_features.cc.tmpl
  • content/renderer/service_worker/embedded_worker_instance_client_impl.cc
  • third_party/blink/renderer/platform/runtime_enabled_features.json5

Estimated timestamp from git blame: 2024-05-16

Background

ProtectedMemory is designed to prevent data-only attacks from modifying security-critical variables (like is_mojo_js_enabled_) by keeping them in read-only memory pages. They are only briefly made writable during legitimate updates via base::AutoWritableMemory.

Vulnerability Details

A structural flaw exists in how RuntimeEnabledFeaturesBase::SetFeatureEnabledFromString is generated by third_party/blink/renderer/build/scripts/templates/runtime_enabled_features.cc.tmpl.

The generator creates a static kFeatures[] array mapping feature string names to their respective setter functions. Crucially, it includes features marked with is_protected_feature: true. The generated setter for a protected feature (e.g., SetMojoJSEnabled) legitimately uses base::AutoWritableMemory to unprotect, modify, and re-protect the memory.

Because this setter is reachable via a string-indexed lookup, it creates a powerful Data-Oriented Programming (DOP) gadget. If an attacker can control a string passed to WebRuntimeFeatures::EnableFeatureFromString, they can safely toggle ProtectedMemory variables without causing a segmentation fault.

Potential Attack Path

Note: These are suggested steps based on static analysis; our tooling does not currently have the ability to run code to produce a live Proof-of-Concept.

  1. Obtain Primitives: An attacker exploits a typical renderer vulnerability (e.g., a V8 bug) to gain arbitrary memory read/write capabilities.
  2. Trigger IPC: The attacker triggers a Service Worker startup sequence, causing the browser to send an EmbeddedWorkerStartParamsPtr IPC to the renderer.
  3. Corrupt Heap Data: During deserialization, the params->forced_enabled_runtime_features std::vector<std::string> is allocated on the renderer’s heap. While the StartWorker task is queued, the attacker uses their write primitive to overwrite the vector’s backing store to point to a crafted string containing "MojoJS".
  4. Execute Gadget: EmbeddedWorkerInstanceClientImpl::StartWorker executes and iterates over the corrupted vector, calling blink::WebRuntimeFeatures::EnableFeatureFromString("MojoJS", true).
  5. Bypass ProtectedMemory: The gadget performs a binary search, finds "MojoJS", and invokes SetMojoJSEnabled(true). base::AutoWritableMemory safely unprotects the page, sets the global is_mojo_js_enabled_ flag to true, and re-protects it.
  6. Evade Context Checks: The attacker creates a new execution context (e.g., a new <iframe>). In Blink IDL, Mojo uses [ContextEnabled=MojoJS, RuntimeEnabled=MojoJS]. The generated bindings (v8_window.cc) use an OR condition for these attributes. Because the global RuntimeEnabled flag is now true, the check short-circuits, completely bypassing the strict ContextFeatureSettings::CrashIfMojoJSNotAllowed() mitigation.
  7. Sandbox Escape: window.Mojo is exposed to the attacker’s JavaScript, allowing them to bind privileged browser interfaces and escape the sandbox.

Suggested Fix

Modify third_party/blink/renderer/build/scripts/templates/runtime_enabled_features.cc.tmpl to explicitly exclude features marked with is_protected_feature: true from the kFeatures[] array used by SetFeatureEnabledFromString (and potentially IsFeatureEnabledFromString). Protected features should not be toggleable via dynamic string lookups.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from 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.

View on issue tracker