Low CVSS 6.5 chrome Policy Bypass 🔧 Commit mapped

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Speech
DescriptionInsufficient policy enforcement in Speech
ComponentSpeech
Bug ClassPolicy Bypass
Tracker519981430
Fix commitc04177aa3181 (chromium/src) +62/-7
CWECWE-602
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-06-04 (per vendor advisory)
Fix landed2026-06-09 (public git)
Disclosed2026-07-29
Patch gap50 days — fix public in source before the advisory

Changed Functions

FunctionChangeNotes
CanRenderFrameHostUseOnDeviceSpeechRecognition
chrome/browser/speech/on_device_speech_recognition_impl.cc
modified
IN_PROC_BROWSER_TEST_F
chrome/browser/speech/on_device_speech_recognition_impl_browsertest.cc
modified

Files Changed

  • chrome/browser/speech/on_device_speech_recognition_impl.cc
  • chrome/browser/speech/on_device_speech_recognition_impl_browsertest.cc
  • content/browser/speech/speech_recognition_dispatcher_host.cc
From c04177aa318139ed030371f07be7c30b479b01ec Mon Sep 17 00:00:00 2001
From: Evan Liu <evliu@google.com>
Date: Mon, 08 Jun 2026 17:11:31 -0700
Subject: [PATCH] Fix StoragePartition bypass in OnDeviceSpeechRecognition

This CL fixes a vulnerability where guest views (e.g. <webview>) could
bypass StoragePartition isolation for on-device Web Speech recognition
by using an `about:blank` iframe to spoof a non-HTTP/HTTPS scheme.

The security checks in `SpeechRecognitionDispatcherHost` and
`OnDeviceSpeechRecognitionImpl` now correctly validate the main frame's
URL and StoragePartition rather than the requesting frame's.
Additionally, guest contexts are explicitly blocked via the main frame's
SecurityPrincipal.

Fixed: 519981430
Change-Id: Ie32e8b2d8b56b1410929429dea6c13c359c19944
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7904795
Reviewed-by: Tsuyoshi Horo <horo@chromium.org>
Commit-Queue: Evan Liu <evliu@google.com>
Cr-Commit-Position: refs/heads/main@{#1643545}
---

diff --git a/chrome/browser/speech/on_device_speech_recognition_impl.cc b/chrome/browser/speech/on_device_speech_recognition_impl.cc
index cac2651..bcbb89f8 100644
--- a/chrome/browser/speech/on_device_speech_recognition_impl.cc
+++ b/chrome/browser/speech/on_device_speech_recognition_impl.cc
@@ -18,6 +18,8 @@
 #include "content/public/browser/document_user_data.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/render_process_host.h"
+#include "content/public/browser/security_principal.h"
+#include "content/public/browser/site_instance.h"
 #include "media/base/media_switches.h"
 #include "media/mojo/mojom/speech_recognizer.mojom.h"
 #include "mojo/public/cpp/bindings/receiver_set.h"
@@ -285,9 +287,16 @@
 
 bool OnDeviceSpeechRecognitionImpl::
     CanRenderFrameHostUseOnDeviceSpeechRecognition() {
-  if (render_frame_host().GetStoragePartition() !=
-      render_frame_host().GetBrowserContext()->GetDefaultStoragePartition()) {
-    return !render_frame_host().GetLastCommittedURL().SchemeIsHTTPOrHTTPS();
+  content::RenderFrameHost* main_frame = render_frame_host().GetMainFrame();
+  if (main_frame->GetSiteInstance()->GetSecurityPrincipal().IsGuest()) {
+    return false;
+  }
+
+  // Allow trusted/special app contexts (like Chrome Extensions and Isolated Web
+  // Apps) that use non-HTTP/HTTPS schemes within custom StoragePartitions.
+  if (main_frame->GetStoragePartition() !=
+      main_frame->GetBrowserContext()->GetDefaultStoragePartition()) {
+    return !main_frame->GetLastCommittedURL().SchemeIsHTTPOrHTTPS();
   }
 
   return true;
diff --git a/chrome/browser/speech/on_device_speech_recognition_impl_browsertest.cc b/chrome/browser/speech/on_device_speech_recognition_impl_browsertest.cc
index 97306f3d..ed73190 100644
--- a/chrome/browser/speech/on_device_speech_recognition_impl_browsertest.cc
+++ b/chrome/browser/speech/on_device_speech_recognition_impl_browsertest.cc
@@ -19,8 +19,10 @@
 #include "chrome/test/base/ui_test_utils.h"
 #include "components/soda/soda_installer.h"
 #include "content/public/browser/document_user_data.h"
+#include "content/public/browser/storage_partition_config.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
 #include "content/public/test/browsing_data_remover_test_util.h"
 #include "media/base/media_switches.h"
 #include "media/mojo/mojom/speech_recognizer.mojom.h"
@@ -169,6 +171,47 @@
                      media::mojom::AvailabilityStatus::kDownloadable));
 }
 
+IN_PROC_BROWSER_TEST_F(OnDeviceSpeechRecognitionImplBrowserTest,
+                       BypassStoragePartitionGuestView) {
+  // Create a custom guest site instance, which uses a non-default storage
+  // partition.
+  scoped_refptr<content::SiteInstance> guest_site_instance =
+      content::SiteInstance::CreateForGuest(
+          browser()->profile(),
+          content::StoragePartitionConfig::Create(
+              browser()->profile(), "my_domain", "my_partition", false));
+
+  content::WebContents::CreateParams params(browser()->profile(),
+                                            guest_site_instance);
+  std::unique_ptr<content::WebContents> guest_contents =
+      content::WebContents::Create(params);
+
+  EXPECT_NE(guest_contents->GetPrimaryMainFrame()->GetStoragePartition(),
+            browser()->profile()->GetDefaultStoragePartition());
+
+  // Navigate to about:blank directly.
+  ASSERT_TRUE(
+      content::NavigateToURL(guest_contents.get(), GURL("about:blank")));
+
+  content::RenderFrameHost* main_frame = guest_contents->GetPrimaryMainFrame();
+  EXPECT_EQ(GURL("about:blank"), main_frame->GetLastCommittedURL());
+
+  auto* speech_impl =
+      OnDeviceSpeechRecognitionImpl::GetOrCreateForCurrentDocument(main_frame);
+  ASSERT_TRUE(speech_impl);
+
+  // The vulnerability allows this to be downloadable.
+  // A correct implementation would return kUnavailable.
+  // We expect it to be kUnavailable to make the test FAIL when the bug is NOT
+  // fixed.
+  speech_impl->Available(
+      {kEnglishLanguageCode}, media::mojom::SpeechRecognitionQuality::kCommand,
+      base::BindOnce(&OnDeviceSpeechRecognitionImplBrowserTest::
+                         OnDeviceWebSpeechAvailableCallbackAndAssertStatus,
+                     base::Unretained(this),
+                     media::mojom::AvailabilityStatus::kUnavailable));
+}
+
 IN_PROC_BROWSER_TEST_F(OnDeviceSpeechRecognitionImplBrowserTest, Install) {
   NavigateToUrl("foo.com");
 
diff --git a/content/browser/speech/speech_recognition_dispatcher_host.cc b/content/browser/speech/speech_recognition_dispatcher_host.cc
index 89e48e8..cafc989 100644
--- a/content/browser/speech/speech_recognition_dispatcher_host.cc
+++ b/content/browser/speech/speech_recognition_dispatcher_host.cc
@@ -19,6 +19,8 @@
 #include "content/public/browser/content_browser_client.h"
 #include "content/public/browser/global_routing_id.h"
 #include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/security_principal.h"
+#include "content/public/browser/site_instance.h"
 #include "content/public/browser/speech_recognition_audio_forwarder_config.h"
 #include "content/public/browser/speech_recognition_manager_delegate.h"
 #include "content/public/browser/speech_recognition_session_config.h"
@@ -168,12 +170,13 @@
   }
 
   content::BrowserContext* browser_context = web_contents->GetBrowserContext();
-  StoragePartition* storage_partition =
-      browser_context->GetStoragePartition(web_contents->GetSiteInstance());
+  content::RenderFrameHost* main_frame = rfh->GetMainFrame();
+  StoragePartition* storage_partition = main_frame->GetStoragePartition();
 
   bool is_valid_storage_context =
-      storage_partition == browser_context->GetDefaultStoragePartition() ||
-      !rfh->GetLastCommittedURL().SchemeIsHTTPOrHTTPS();
+      !main_frame->GetSiteInstance()->GetSecurityPrincipal().IsGuest() &&
+      (storage_partition == browser_context->GetDefaultStoragePartition() ||
+       !main_frame->GetLastCommittedURL().SchemeIsHTTPOrHTTPS());
   bool is_policy_enabled = rfh->IsFeatureEnabled(
       network::mojom::PermissionsPolicyFeature::kOnDeviceSpeechRecognition);
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/speech/on_device_speech_recognition_impl_browsertest.cc b/chrome/browser/speech/on_device_speech_recognition_impl_browsertest.cc
index 97306f3d..ed73190 100644
--- a/chrome/browser/speech/on_device_speech_recognition_impl_browsertest.cc
+++ b/chrome/browser/speech/on_device_speech_recognition_impl_browsertest.cc
@@ -19,8 +19,10 @@
 #include "chrome/test/base/ui_test_utils.h"
 #include "components/soda/soda_installer.h"
 #include "content/public/browser/document_user_data.h"
+#include "content/public/browser/storage_partition_config.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
 #include "content/public/test/browsing_data_remover_test_util.h"
 #include "media/base/media_switches.h"
 #include "media/mojo/mojom/speech_recognizer.mojom.h"
@@ -169,6 +171,47 @@
                      media::mojom::AvailabilityStatus::kDownloadable));
 }
 
+IN_PROC_BROWSER_TEST_F(OnDeviceSpeechRecognitionImplBrowserTest,
+                       BypassStoragePartitionGuestView) {
+  // Create a custom guest site instance, which uses a non-default storage
+  // partition.
+  scoped_refptr<content::SiteInstance> guest_site_instance =
+      content::SiteInstance::CreateForGuest(
+          browser()->profile(),
+          content::StoragePartitionConfig::Create(
+              browser()->profile(), "my_domain", "my_partition", false));
+
+  content::WebContents::CreateParams params(browser()->profile(),
+                                            guest_site_instance);
+  std::unique_ptr<content::WebContents> guest_contents =
+      content::WebContents::Create(params);
+
+  EXPECT_NE(guest_contents->GetPrimaryMainFrame()->GetStoragePartition(),
+            browser()->profile()->GetDefaultStoragePartition());
+
+  // Navigate to about:blank directly.
+  ASSERT_TRUE(
+      content::NavigateToURL(guest_contents.get(), GURL("about:blank")));
+
+  content::RenderFrameHost* main_frame = guest_contents->GetPrimaryMainFrame();
+  EXPECT_EQ(GURL("about:blank"), main_frame->GetLastCommittedURL());
+
+  auto* speech_impl =
+      OnDeviceSpeechRecognitionImpl::GetOrCreateForCurrentDocument(main_frame);
+  ASSERT_TRUE(speech_impl);
+
+  // The vulnerability allows this to be downloadable.
+  // A correct implementation would return kUnavailable.
+  // We expect it to be kUnavailable to make the test FAIL when the bug is NOT
+  // fixed.
+  speech_impl->Available(
+      {kEnglishLanguageCode}, media::mojom::SpeechRecognitionQuality::kCommand,
+      base::BindOnce(&OnDeviceSpeechRecognitionImplBrowserTest::
+                         OnDeviceWebSpeechAvailableCallbackAndAssertStatus,
+                     base::Unretained(this),
+                     media::mojom::AvailabilityStatus::kUnavailable));
+}
+
 IN_PROC_BROWSER_TEST_F(OnDeviceSpeechRecognitionImplBrowserTest, Install) {
   NavigateToUrl("foo.com");
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.