Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Speech
DescriptionInsufficient validation of untrusted input in Speech
ComponentSpeech
Bug ClassLogic Error
Tracker517376041
Fix commit1eb42257b1f3 (chromium/src) +239/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
EventThunk
content/browser/speech/speech_synthesis_impl.cc
modified
is_audible_
content/browser/speech/speech_synthesis_impl.cc
modified
switch
content/browser/speech/speech_synthesis_impl.cc
modified
if
content/browser/speech/speech_synthesis_impl.cc
modified
FakeTtsPlatform
content/browser/speech/speech_synthesis_impl_browsertest.cc
modified
SpeechSynthesisBrowserTest
content/browser/speech/speech_synthesis_impl_browsertest.cc
modified
MockSpeechSynthesisClient
content/browser/speech/speech_synthesis_impl_browsertest.cc
modified
BindNewPipeAndPassRemote
content/browser/speech/speech_synthesis_impl_browsertest.cc
modified

Files Changed

  • content/browser/speech/speech_synthesis_impl.cc
  • content/browser/speech/speech_synthesis_impl_browsertest.cc
From 1eb42257b1f3d090cefad6d368ff22b9c9d282f6 Mon Sep 17 00:00:00 2001
From: Evan Liu <evliu@google.com>
Date: Mon, 01 Jun 2026 13:41:25 -0700
Subject: [PATCH] Gate SpeechSynthesis audibility on non-zero volume

SpeechSynthesisImpl previously unconditionally registered the frame as
audible upon starting speech, regardless of volume. This allowed a
compromised renderer to trigger a silent SpeechSynthesis request (volume
= 0.0) to spoof tab audibility and bypass browser-side autoplay/gesture
restrictions (e.g., gaining silent Wake Locks or Auto-PiP).

This CL ensures SpeechSynthesis only registers as audible in the browser
process if the volume is default (-1.0) or explicitly greater than 0.0.

It also adds a browser integration test under content_browsertests to
verify the logic.

Fixed=517376041

Change-Id: I86d362ac61d1f1c3e6079529478ace06a11979ed
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7882174
Reviewed-by: Katie D <katie@chromium.org>
Commit-Queue: Evan Liu <evliu@google.com>
Cr-Commit-Position: refs/heads/main@{#1639638}
---

diff --git a/content/browser/speech/speech_synthesis_impl.cc b/content/browser/speech/speech_synthesis_impl.cc
index 1f07dd1..daefb651 100644
--- a/content/browser/speech/speech_synthesis_impl.cc
+++ b/content/browser/speech/speech_synthesis_impl.cc
@@ -21,8 +21,11 @@
 class EventThunk : public UtteranceEventDelegate {
  public:
   EventThunk(mojo::PendingRemote<blink::mojom::SpeechSynthesisClient> client,
-             AudibleCB audible_cb)
-      : client_(std::move(client)), audible_cb_(std::move(audible_cb)) {}
+             AudibleCB audible_cb,
+             bool is_audible)
+      : client_(std::move(client)),
+        audible_cb_(std::move(audible_cb)),
+        is_audible_(is_audible) {}
   ~EventThunk() override = default;
 
   // UtteranceEventDelegate methods:
@@ -38,7 +41,9 @@
 
     switch (event_type) {
       case TTS_EVENT_START:
-        audible_client_ = audible_cb_.Run();
+        if (is_audible_) {
+          audible_client_ = audible_cb_.Run();
+        }
         client_->OnStartedSpeaking();
         break;
       case TTS_EVENT_END:
@@ -75,7 +80,9 @@
         client_->OnPausedSpeaking();
         break;
       case TTS_EVENT_RESUME:
-        audible_client_ = audible_cb_.Run();
+        if (is_audible_) {
+          audible_client_ = audible_cb_.Run();
+        }
         client_->OnResumedSpeaking();
         break;
     }
@@ -86,6 +93,7 @@
   AudibleCB audible_cb_;
   std::unique_ptr<AudioStreamMonitor::AudibleClientRegistration>
       audible_client_;
+  bool is_audible_;
 };
 
 void SendVoiceListToObserver(
@@ -158,6 +166,10 @@
   tts_utterance->SetContinuousParameters(utterance->rate, utterance->pitch,
                                          utterance->volume);
 
+  bool is_audible =
+      (utterance->volume == blink::mojom::kSpeechSynthesisDoublePrefNotSet) ||
+      (utterance->volume > 0.0);
+
   // See comments on EventThunk about how lifetime of this instance is managed.
   tts_utterance->SetEventDelegate(std::make_unique<EventThunk>(
       std::move(client),
@@ -165,7 +177,8 @@
           &AudioStreamMonitor::RegisterAudibleClient,
           base::Unretained(static_cast<WebContentsImpl*>(web_contents_)
                                ->audio_stream_monitor()),
-          frame_id_)));
+          frame_id_),
+      is_audible));
 
   TtsController::GetInstance()->SpeakOrEnqueue(std::move(tts_utterance));
 }
diff --git a/content/browser/speech/speech_synthesis_impl_browsertest.cc b/content/browser/speech/speech_synthesis_impl_browsertest.cc
new file mode 100644
index 0000000..c44723b3
--- /dev/null
+++ b/content/browser/speech/speech_synthesis_impl_browsertest.cc
@@ -0,0 +1,220 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/run_loop.h"
+#include "base/task/single_thread_task_runner.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
+#include "content/browser/speech/tts_controller_impl.h"
+#include "content/browser/web_contents/web_contents_impl.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/tts_controller.h"
+#include "content/public/browser/tts_platform.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/content_browser_test.h"
+#include "content/public/test/content_browser_test_utils.h"
+#include "content/shell/browser/shell.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/speech/speech_synthesis.mojom.h"
+
+namespace content {
+
+// A fake TTS platform implementation to simulate starting speech events in
+// tests.
+class FakeTtsPlatform : public TtsPlatform {
+ public:
+  FakeTtsPlatform() = default;
+  ~FakeTtsPlatform() = default;
+
+  bool PlatformImplSupported() override { return true; }
+  bool PlatformImplInitialized() override { return true; }
+  void LoadBuiltInTtsEngine(BrowserContext* browser_context) override {}
+
+  void Speak(
+      int utterance_id,
+      const std::string& utterance,
+      const std::string& lang,
+      const VoiceData& voice,
+      const UtteranceContinuousParameters& params,
+      base::OnceCallback<void(bool)> did_start_speaking_callback) override {
+    // Accept the speak request.
+    std::move(did_start_speaking_callback).Run(true);
+
+    // Post a task to simulate the asynchronous firing of the start event from
+    // the OS engine.
+    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+        FROM_HERE, base::BindOnce(&FakeTtsPlatform::FireStartEvent,
+                                  base::Unretained(this), utterance_id));
+  }
+
+  bool StopSpeaking() override { return true; }
+  bool IsSpeaking() override { return false; }
+
+  void GetVoices(std::vector<VoiceData>* out_voices) override {
+    VoiceData voice;
+    voice.name = "Fake Voice";
+    voice.lang = "en-US";
+    voice.native = true;
+    out_voices->push_back(voice);
+  }
+
+  void Pause() override {}
+  void Resume() override {}
+  void WillSpeakUtteranceWithVoice(TtsUtterance* utterance,
+                                   const VoiceData& voice_data) override {}
+  std::string GetError() override { return ""; }
+  void ClearError() override {}
+  void SetError(const std::string& error) override {}
+  void Shutdown() override {}
+  void FinalizeVoiceOrdering(std::vector<VoiceData>& voices) override {}
+  void RefreshVoices() override {}
+
+ private:
+  void FireStartEvent(int utterance_id) {
+    TtsController::GetInstance()->OnTtsEvent(
+        utterance_id, TTS_EVENT_START, /*char_index=*/0, /*length=*/0, "");
+  }
+};
+
+class SpeechSynthesisBrowserTest : public ContentBrowserTest {
+ public:
+  SpeechSynthesisBrowserTest() = default;
+  ~SpeechSynthesisBrowserTest() override = default;
+
+  void TearDownOnMainThread() override {
+    TtsController::GetInstance()->SetTtsPlatform(nullptr);
+    ContentBrowserTest::TearDownOnMainThread();
+  }
+};
+
+class MockSpeechSynthesisClient : public blink::mojom::SpeechSynthesisClient {
+ public:
+  MockSpeechSynthesisClient() = default;
+  ~MockSpeechSynthesisClient() override = default;
+
+  mojo::PendingRemote<blink::mojom::SpeechSynthesisClient>
+  BindNewPipeAndPassRemote() {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/speech/speech_synthesis_impl_browsertest.cc b/content/browser/speech/speech_synthesis_impl_browsertest.cc
new file mode 100644
index 0000000..c44723b3
--- /dev/null
+++ b/content/browser/speech/speech_synthesis_impl_browsertest.cc
@@ -0,0 +1,220 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/run_loop.h"
+#include "base/task/single_thread_task_runner.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
+#include "content/browser/speech/tts_controller_impl.h"
+#include "content/browser/web_contents/web_contents_impl.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/tts_controller.h"
+#include "content/public/browser/tts_platform.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/content_browser_test.h"
+#include "content/public/test/content_browser_test_utils.h"
+#include "content/shell/browser/shell.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/speech/speech_synthesis.mojom.h"
+
+namespace content {
+
+// A fake TTS platform implementation to simulate starting speech events in
+// tests.
+class FakeTtsPlatform : public TtsPlatform {
+ public:
+  FakeTtsPlatform() = default;
+  ~FakeTtsPlatform() = default;
+
+  bool PlatformImplSupported() override { return true; }
+  bool PlatformImplInitialized() override { return true; }
+  void LoadBuiltInTtsEngine(BrowserContext* browser_context) override {}
+
+  void Speak(
+      int utterance_id,
+      const std::string& utterance,
+      const std::string& lang,
+      const VoiceData& voice,
+      const UtteranceContinuousParameters& params,
+      base::OnceCallback<void(bool)> did_start_speaking_callback) override {
+    // Accept the speak request.
+    std::move(did_start_speaking_callback).Run(true);
+
+    // Post a task to simulate the asynchronous firing of the start event from
+    // the OS engine.
+    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+        FROM_HERE, base::BindOnce(&FakeTtsPlatform::FireStartEvent,
+                                  base::Unretained(this), utterance_id));
+  }
+
+  bool StopSpeaking() override { return true; }
+  bool IsSpeaking() override { return false; }
+
+  void GetVoices(std::vector<VoiceData>* out_voices) override {
+    VoiceData voice;
+    voice.name = "Fake Voice";
+    voice.lang = "en-US";
+    voice.native = true;
+    out_voices->push_back(voice);
+  }
+
+  void Pause() override {}
+  void Resume() override {}
+  void WillSpeakUtteranceWithVoice(TtsUtterance* utterance,
+                                   const VoiceData& voice_data) override {}
+  std::string GetError() override { return ""; }
+  void ClearError() override {}
+  void SetError(const std::string& error) override {}
+  void Shutdown() override {}
+  void FinalizeVoiceOrdering(std::vector<VoiceData>& voices) override {}
+  void RefreshVoices() override {}
+
+ private:
+  void FireStartEvent(int utterance_id) {
+    TtsController::GetInstance()->OnTtsEvent(
+        utterance_id, TTS_EVENT_START, /*char_index=*/0, /*length=*/0, "");
+  }
+};
+
+class SpeechSynthesisBrowserTest : public ContentBrowserTest {
+ public:
+  SpeechSynthesisBrowserTest() = default;
+  ~SpeechSynthesisBrowserTest() override = default;
+
+  void TearDownOnMainThread() override {
+    TtsController::GetInstance()->SetTtsPlatform(nullptr);
+    ContentBrowserTest::TearDownOnMainThread();
+  }
+};
+
+class MockSpeechSynthesisClient : public blink::mojom::SpeechSynthesisClient {
+ public:
+  MockSpeechSynthesisClient() = default;
+  ~MockSpeechSynthesisClient() override = default;
+
+  mojo::PendingRemote<blink::mojom::SpeechSynthesisClient>
+  BindNewPipeAndPassRemote() {
+    return receiver_.BindNewPipeAndPassRemote();
+  }
+
+  // blink::mojom::SpeechSynthesisClient implementation:
+  void OnStartedSpeaking() override {
+    started_ = true;
+    if (quit_closure_) {
+      std::move(quit_closure_).Run();
+    }
+  }
+
+  void OnFinishedSpeaking(
+      blink::mojom::SpeechSynthesisErrorCode error_code) override {}
+  void OnPausedSpeaking() override {}
+  void OnResumedSpeaking() override {}
+  void OnEncounteredWordBoundary(uint32_t char_index,
+                                 uint32_t char_length) override {}
+  void OnEncounteredSentenceBoundary(uint32_t char_index,
+                                     uint32_t char_length) override {}
+  void OnEncounteredSpeakingError() override {}
+
+  void WaitForStart() {
+    if (started_) {
+      return;
+    }
+    base::RunLoop run_loop;
+    quit_closure_ = run_loop.QuitClosure();
+    run_loop.Run();
+  }
+
+ private:
+  bool started_ = false;
+  base::OnceClosure quit_closure_;
+  mojo::Receiver<blink::mojom::SpeechSynthesisClient> receiver_{this};
+};
+
+IN_PROC_BROWSER_TEST_F(SpeechSynthesisBrowserTest,
+                       SilentSpeechDoesNotTriggerAudibility) {
+  // Navigate to a fake page to properly initialize the frame document and
+  // PerformanceManager node.
+  ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
+
+  WebContentsImpl* web_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+  RenderFrameHostImpl* rfh =
+      static_cast<RenderFrameHostImpl*>(web_contents->GetPrimaryMainFrame());
+
+  web_contents->SetAudioMuted(false);
+  web_contents->WasShown();
+
+  // Register the fake platform TTS engine.
+  FakeTtsPlatform fake_platform;
+  TtsController::GetInstance()->SetTtsPlatform(&fake_platform);
+  static_cast<TtsControllerImpl*>(TtsController::GetInstance())
+      ->SetStopSpeakingWhenHidden(false);
+
+  mojo::Remote<blink::mojom::SpeechSynthesis> speech_synthesis;
+  rfh->GetSpeechSynthesis(speech_synthesis.BindNewPipeAndPassReceiver());
+
+  ASSERT_TRUE(speech_synthesis.is_bound());
+  ASSERT_FALSE(rfh->HasTransientUserActivation());
+
+  // 1. Silent volume
+  {
+    auto utterance = blink::mojom::SpeechSynthesisUtterance::New();
+    utterance->text = "Silent";
+    utterance->lang = "en-US";
+    utterance->volume = 0.0;
+
+    MockSpeechSynthesisClient client;
+    speech_synthesis->Speak(std::move(utterance),
+                            client.BindNewPipeAndPassRemote());
+    client.WaitForStart();
+
+    // Verify that silent SpeechSynthesis (volume = 0.0) does not incorrectly
+    // register the web contents as audible.
+    EXPECT_FALSE(web_contents->IsCurrentlyAudible());
+
+    TtsController::GetInstance()->Stop();
+  }
+
+  // 2. Default volume
+  {
+    auto utterance = blink::mojom::SpeechSynthesisUtterance::New();
+    utterance->text = "Default volume";
+    utterance->lang = "en-US";
+    utterance->volume = blink::mojom::kSpeechSynthesisDoublePrefNotSet;
+
+    MockSpeechSynthesisClient client;
+    speech_synthesis->Speak(std::move(utterance),
+                            client.BindNewPipeAndPassRemote());
+    client.WaitForStart();
+
+    // Verify that default SpeechSynthesis correctly registers the web contents
+    // as audible.
+    EXPECT_TRUE(web_contents->IsCurrentlyAudible());
+
+    TtsController::GetInstance()->Stop();
+  }
+
+  // 3. Explicit non-zero volume
+  {
+    auto utterance = blink::mojom::SpeechSynthesisUtterance::New();
+    utterance->text = "Loud volume";
+    utterance->lang = "en-US";
+    utterance->volume = 1.0;
+
+    MockSpeechSynthesisClient client;
+    speech_synthesis->Speak(std::move(utterance),
+                            client.BindNewPipeAndPassRemote());
+    client.WaitForStart();
+
+    // Verify that non-zero volume SpeechSynthesis correctly registers the web
+    // contents as audible.
+    EXPECT_TRUE(web_contents->IsCurrentlyAudible());
+
+    TtsController::GetInstance()->Stop();
+  }
+}
+
+}  // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 040fb038..ea4954e 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2319,6 +2319,7 @@
       "../browser/font_preferences_browsertest.cc",
       "../browser/renderer_host/clipboard_host_impl_browsertest.cc",
       "../browser/speech/speech_recognition_browsertest.cc",
+      "../browser/speech/speech_synthesis_impl_browsertest.cc",
       "../browser/speech/tts_ssml_browsertest.cc",
       "../browser/storage_service_restart_browsertest.cc",
       "../browser/storage_service_sandbox_browsertest.cc",
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.