Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in LiveCaption
DescriptionOut of bounds read in LiveCaption
ComponentLiveCaption
Bug ClassOOB
Tracker504180386
Fix commita1ab0fb6b996 (chromium/src) +46/-11
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
TEST_F
components/live_caption/google_api_translation_dispatcher_unittest.cc
modified
BindLambdaForTesting
components/live_caption/google_api_translation_dispatcher_unittest.cc
modified

Files Changed

  • components/live_caption/google_api_translation_dispatcher.cc
  • components/live_caption/google_api_translation_dispatcher_unittest.cc
From a1ab0fb6b9962a54d1b0ef20c57e07e08aa5e8c0 Mon Sep 17 00:00:00 2001
From: Evan Liu <[email protected]>
Date: Fri, 24 Apr 2026 17:55:50 -0700
Subject: [PATCH] Fix JSON injection and OOB read in Live Caption Translation

This CL addresses a vulnerability in
`GoogleApiTranslationDispatcher::GetTranslation` where the POST request
body for the Google Cloud Translate API was constructed using
`base::StringPrintf` without proper JSON escaping. This allowed remote
peers to inject arbitrary JSON keys or cause a denial of service by
sending malformed transcript text or locale identifiers.

Additionally, this change resolves a latent memory safety issue where
`std::string_view::data()` was unsafely passed to `%s` format
specifiers, risking out-of-bounds reads if the string views were not
null-terminated.

The fix replaces `base::StringPrintf` with `base::DictValue` and
`base::WriteJson` to ensure all parameters are properly escaped and
bounds-checked during serialization. A unit test
(`JsonEscapeTranscriptText`) has been added to verify that malicious
input containing quotation marks and newlines is safely handled.

Fixed: 504180386
Change-Id: I0c66342291e573025a671df5fce4f9561e13da56
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7793637
Commit-Queue: Evan Liu <[email protected]>
Reviewed-by: Tommy Steimel <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1620582}
---

diff --git a/components/live_caption/google_api_translation_dispatcher.cc b/components/live_caption/google_api_translation_dispatcher.cc
index 13484c5c..1fb029dc 100644
--- a/components/live_caption/google_api_translation_dispatcher.cc
+++ b/components/live_caption/google_api_translation_dispatcher.cc
@@ -9,6 +9,7 @@
 #include <utility>
 
 #include "base/functional/bind.h"
+#include "base/json/json_writer.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/metrics/metrics_hashes.h"
 #include "base/strings/string_number_conversions.h"
@@ -33,13 +34,6 @@
 
 // Request constants.
 const size_t kMaxMessageSize = 1024 * 1024;  // 1MB
-constexpr char kTranslateBodyRequestTemplate[] =
-    "{"
-    "\"q\":\"%s\","
-    "\"source\":\"%s\","
-    "\"target\":\"%s\","
-    "\"format\":\"text\""
-    "}";
 constexpr char kTranslateUrl[] =
     "https://translation.googleapis.com/language/translate/v2?key=%s";
 constexpr char kUploadContentType[] = "application/json";
@@ -148,10 +142,17 @@
         })");
   url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
                                                  traffic_annotation);
-  url_loader_->AttachStringForUpload(
-      base::StringPrintf(kTranslateBodyRequestTemplate, result.data(),
-                         source_language.data(), target_language.data()),
-      kUploadContentType);
+
+  base::DictValue request_body;
+  request_body.Set("q", result);
+  request_body.Set("source", source_language);
+  request_body.Set("target", target_language);
+  request_body.Set("format", "text");
+
+  std::optional<std::string> request_body_str = base::WriteJson(request_body);
+
+  url_loader_->AttachStringForUpload(request_body_str.value_or(""),
+                                     kUploadContentType);
 
   // Unretained is safe because |this| owns |url_loader_|.
   url_loader_->DownloadToString(
diff --git a/components/live_caption/google_api_translation_dispatcher_unittest.cc b/components/live_caption/google_api_translation_dispatcher_unittest.cc
index ee1ed07e..1676ca7a 100644
--- a/components/live_caption/google_api_translation_dispatcher_unittest.cc
+++ b/components/live_caption/google_api_translation_dispatcher_unittest.cc
@@ -4,6 +4,7 @@
 
 #include "components/live_caption/google_api_translation_dispatcher.h"
 
+#include "base/json/json_reader.h"
 #include "base/run_loop.h"
 #include "base/test/bind.h"
 #include "base/test/mock_callback.h"
@@ -12,6 +13,7 @@
 #include "services/network/public/cpp/url_loader_completion_status.h"
 #include "services/network/public/mojom/url_response_head.mojom.h"
 #include "services/network/test/test_url_loader_factory.h"
+#include "services/network/test/test_utils.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -143,4 +145,36 @@
   translate_callback_run_loop.Run();
 }
 
+TEST_F(GoogleApiTranslationDispatcherTest, JsonEscapeTranscriptText) {
+  base::RunLoop wait_for_translation_request;
+  base::MockCallback<TranslateEventCallback> translate_callback;
+
+  test_url_loader_factory_.SetInterceptor(
+      base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
+        wait_for_translation_request.Quit();
+      }));
+
+  GetTranslation("hello \"world\", \n \"injected\": \"yes\"", "es", "en",
+                 translate_callback);
+  wait_for_translation_request.Run();
+
+  network::TestURLLoaderFactory::PendingRequest* pending_request =
+      GetPendingRequest();
+  ASSERT_TRUE(pending_request);
+
+  std::string upload_data = network::GetUploadData(pending_request->request);
+  std::optional<base::Value> parsed_json =
+      base::JSONReader::Read(upload_data, base::JSON_PARSE_RFC);
+  ASSERT_TRUE(parsed_json);
+  ASSERT_TRUE(parsed_json->is_dict());
+
+  const std::string* q_val = parsed_json->GetDict().FindString("q");
+  ASSERT_TRUE(q_val);
+  EXPECT_EQ(*q_val, "hello \"world\", \n \"injected\": \"yes\"");
+
+  const std::string* injected_val =
+      parsed_json->GetDict().FindString("injected");
+  EXPECT_FALSE(injected_val);
+}
+
 }  // namespace captions
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/live_caption/google_api_translation_dispatcher_unittest.cc b/components/live_caption/google_api_translation_dispatcher_unittest.cc
index ee1ed07e..1676ca7a 100644
--- a/components/live_caption/google_api_translation_dispatcher_unittest.cc
+++ b/components/live_caption/google_api_translation_dispatcher_unittest.cc
@@ -4,6 +4,7 @@
 
 #include "components/live_caption/google_api_translation_dispatcher.h"
 
+#include "base/json/json_reader.h"
 #include "base/run_loop.h"
 #include "base/test/bind.h"
 #include "base/test/mock_callback.h"
@@ -12,6 +13,7 @@
 #include "services/network/public/cpp/url_loader_completion_status.h"
 #include "services/network/public/mojom/url_response_head.mojom.h"
 #include "services/network/test/test_url_loader_factory.h"
+#include "services/network/test/test_utils.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -143,4 +145,36 @@
   translate_callback_run_loop.Run();
 }
 
+TEST_F(GoogleApiTranslationDispatcherTest, JsonEscapeTranscriptText) {
+  base::RunLoop wait_for_translation_request;
+  base::MockCallback<TranslateEventCallback> translate_callback;
+
+  test_url_loader_factory_.SetInterceptor(
+      base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
+        wait_for_translation_request.Quit();
+      }));
+
+  GetTranslation("hello \"world\", \n \"injected\": \"yes\"", "es", "en",
+                 translate_callback);
+  wait_for_translation_request.Run();
+
+  network::TestURLLoaderFactory::PendingRequest* pending_request =
+      GetPendingRequest();
+  ASSERT_TRUE(pending_request);
+
+  std::string upload_data = network::GetUploadData(pending_request->request);
+  std::optional<base::Value> parsed_json =
+      base::JSONReader::Read(upload_data, base::JSON_PARSE_RFC);
+  ASSERT_TRUE(parsed_json);
+  ASSERT_TRUE(parsed_json->is_dict());
+
+  const std::string* q_val = parsed_json->GetDict().FindString("q");
+  ASSERT_TRUE(q_val);
+  EXPECT_EQ(*q_val, "hello \"world\", \n \"injected\": \"yes\"");
+
+  const std::string* injected_val =
+      parsed_json->GetDict().FindString("injected");
+  EXPECT_FALSE(injected_val);
+}
+
 }  // namespace captions
Loading diff…

Original Bug Report

reported by [email protected]

Potential JSON injection in Live Caption translation dispatcher

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: The GoogleApiTranslationDispatcher uses base::StringPrintf to construct JSON request bodies without escaping input strings. A remote peer in a ChromeOS Boca session can inject arbitrary JSON keys via transcript messages, leading to API parameter injection or a localized denial of service. The code also contains a fragile pattern using std::string_view::data() with %s.

Affected files:

  • components/live_caption/google_api_translation_dispatcher.cc
  • chromeos/ash/components/boca/babelorca/babel_orca_caption_translator.cc
  • chromeos/ash/components/boca/babelorca/babel_orca_consumer.cc

Estimated timestamp from git blame: 2025-12-02

Description

There is a potential JSON injection vulnerability in GoogleApiTranslationDispatcher::GetTranslation (components/live_caption/google_api_translation_dispatcher.cc). The function constructs a POST request body for the Google Cloud Translate API by interpolating strings into a JSON template using base::StringPrintf:

constexpr char kTranslateBodyRequestTemplate[] =
    "{\"q\":\"%s\",\"source\":\"%s\",\"target\":\"%s\",\"format\":\"text\"}";
url_loader_->AttachStringForUpload(
    base::StringPrintf(kTranslateBodyRequestTemplate, result.data(),
                       source_language.data(), target_language.data()),
    kUploadContentType);

Because no JSON escaping is performed on result, source_language, or target_language, an attacker who controls these values can break out of the string boundaries by injecting characters like " and ,.

This code path is reachable by a remote peer in a ChromeOS Boca (School Tools) session. A producer (e.g., a teacher device) sends transcript messages (BabelOrcaMessage) via a Tachyon stream. The transcript text is parsed, placed into a media::SpeechRecognitionResult, and passed down to BabelOrcaCaptionTranslator::Translate and eventually to GoogleApiTranslationDispatcher::GetTranslation without any sanitization.

While the source_language and target_language parameters are validated via l10n_util::IsValidLocaleSyntax, this validation only strictly checks the prefix before an @ symbol. An attacker can supply a locale like en@k="x which passes validation but still permits JSON injection.

Impact

An authenticated remote producer can inject arbitrary JSON structures into the request body sent from the consumer’s browser process to translation.googleapis.com. Since the request omits user credentials (CredentialsMode::kOmit) and relies on a hardcoded API key, the impact is isolated to the translation feature itself:

  1. Parameter Injection: An attacker can inject or override top-level JSON keys in the API request (e.g., supplying a transcript like text","target":"ru).
  2. Functional Denial of Service: Injecting invalid JSON syntax will cause the Cloud Translate API to return a 400 Bad Request error, breaking the live translation feature for the consumer.

Latent Code Health Issue

The result, source_language, and target_language arguments in GetTranslation are typed as std::string_view. Passing .data() from a std::string_view to a %s format specifier is unsafe as it does not guarantee null-termination, which could lead to an out-of-bounds read.

Codebase analysis confirms that all current callers (including BabelOrcaCaptionTranslator) pass std::string objects that are implicitly converted to std::string_view, meaning the buffers are safely null-terminated in practice today. However, this is a fragile pattern that could become an exploitable vulnerability if new callers are introduced.

Potential Reproduction Steps

Note: These are suggested steps based on static analysis; our tooling has not yet executed a working proof-of-concept.

  1. Establish a ChromeOS Boca session with an attacker acting as the producer (teacher) and a victim as the consumer (student).
  2. From the producer device, send a BabelOrcaMessage with transcript text containing JSON-breaking characters, such as: test text","target":"ru.
  3. The consumer’s browser process will receive the transcript and dispatch it for translation.
  4. Observe the outbound network request to translation.googleapis.com. The body will contain the hijacked JSON: {"q":"test text","target":"ru","source":"...","target":"...","format":"text"}.

Suggested Fix

  1. Construct JSON Safely: Replace base::StringPrintf with base::Value::Dict to build the request parameters, and use base::JSONWriter::Write to serialize it to a string. This ensures all values are properly escaped.
  2. Fix String View Usage: Change the parameter types in GoogleApiTranslationDispatcher::GetTranslation from std::string_view to const std::string&, or explicitly convert the std::string_view to a std::string before use, preventing potential out-of-bounds reads if a non-null-terminated view is ever passed.

Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646


Results 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