CVE-2026-11301
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcomponents/live_caption/google_api_translation_dispatcher_unittest.cc |
modified | |
BindLambdaForTestingcomponents/live_caption/google_api_translation_dispatcher_unittest.cc |
modified |
Files Changed
components/live_caption/google_api_translation_dispatcher.cccomponents/live_caption/google_api_translation_dispatcher_unittest.cc
Patch
From a1ab0fb6b9962a54d1b0ef20c57e07e08aa5e8c0 Mon Sep 17 00:00:00 2001
From: Evan Liu <evliu@google.com>
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 <evliu@google.com>
Reviewed-by: Tommy Steimel <steimel@chromium.org>
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
Regression Test / PoC
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
Original Bug Report
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.ccchromeos/ash/components/boca/babelorca/babel_orca_caption_translator.ccchromeos/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:
- 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). - 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.
- Establish a ChromeOS Boca session with an attacker acting as the producer (teacher) and a victim as the consumer (student).
- From the producer device, send a
BabelOrcaMessagewith transcript text containing JSON-breaking characters, such as:test text","target":"ru. - The consumer’s browser process will receive the transcript and dispatch it for translation.
- 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
- Construct JSON Safely: Replace
base::StringPrintfwithbase::Value::Dictto build the request parameters, and usebase::JSONWriter::Writeto serialize it to a string. This ensures all values are properly escaped. - Fix String View Usage: Change the parameter types in
GoogleApiTranslationDispatcher::GetTranslationfromstd::string_viewtoconst std::string&, or explicitly convert thestd::string_viewto astd::stringbefore 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.