Chrome · Speech
CVE-2026-17985
Logic Error in Speech
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
CanRenderFrameHostUseOnDeviceSpeechRecognitionchrome/browser/speech/on_device_speech_recognition_impl.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/speech/on_device_speech_recognition_impl_browsertest.cc |
modified |
Files Changed
chrome/browser/speech/on_device_speech_recognition_impl.ccchrome/browser/speech/on_device_speech_recognition_impl_browsertest.cccontent/browser/speech/speech_recognition_dispatcher_host.cc
Patch
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.
References
On This Page