CVE-2026-11691
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forchrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl.cc |
modified | |
ifchrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl.cc |
modified | |
TEST_Fchrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl_unittest.cc |
modified |
Files Changed
chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl.ccchrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl_unittest.cc
Patch
From d55570905a1471af7eed00c9ee633af06f68595f Mon Sep 17 00:00:00 2001 From: Jilin Yang <[email protected]> Date: Fri, 29 May 2026 14:40:05 -0700 Subject: [PATCH] Escape async query param in OneGoogleBar URL construction GetApiUrl() concatenated the render-supplied "async" value into the request query unescaped, allowing query-parameter injection into the credentialed newtab_ogb request. Escape it with EscapeQueryParamValue, restoring only the ':' and ',' then backend needs literal. Fixed: b:517585486 Change-Id: I61d6b8e519f2651dc89f42fbb0463b2adfdaf796 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7880156 Reviewed-by: Riley Tatum <[email protected]> Commit-Queue: Jilin Yang <[email protected]> Cr-Commit-Position: refs/heads/main@{#1638742} --- diff --git a/chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl.cc b/chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl.cc index a2ee72c..bfd5f86d 100644 --- a/chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl.cc +++ b/chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl.cc @@ -13,6 +13,7 @@ #include "base/functional/bind.h" #include "base/functional/callback.h" #include "base/json/json_writer.h" +#include "base/strings/escape.h" #include "base/strings/string_util.h" #include "base/values.h" #include "build/build_config.h" @@ -336,13 +337,16 @@ } for (const auto& param_pair : additional_query_params_) { - // Add the "async=" parameter. We can't use net::AppendQueryParameter for - // this because we need the ":" to remain unescaped. + // Escape to neutralize injection chars (e.g., '&', '='), then restore the + // ':' and ',' that the backend requires to stay literal in "async". if (param_pair.first == "async") { - std::string query = api_url.GetQuery() + "&async=" + param_pair.second; - if (query.at(0) == '&') { - query = query.substr(1); - } + std::string async_value = + base::EscapeQueryParamValue(param_pair.second, /*use_plus*/ true); + base::ReplaceSubstringsAfterOffset(&async_value, 0, "%2C", ","); + base::ReplaceSubstringsAfterOffset(&async_value, 0, "%3A", ":"); + std::string query = api_url.GetQuery(); + query = query.empty() ? "async=" + async_value + : query + "&async=" + async_value; GURL::Replacements replacements; replacements.SetQueryStr(query); api_url = api_url.ReplaceComponents(replacements); diff --git a/chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl_unittest.cc b/chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl_unittest.cc index 0c1f88ee..305527e8 100644 --- a/chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl_unittest.cc +++ b/chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl_unittest.cc @@ -21,6 +21,7 @@ #include "components/signin/core/browser/signin_header_helper.h" #include "components/variations/scoped_variations_ids_provider.h" #include "content/public/test/browser_task_environment.h" +#include "net/base/url_util.h" #include "net/http/http_request_headers.h" #include "net/http/http_status_code.h" #include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h" @@ -182,6 +183,36 @@ one_google_bar_loader()->GetLoadURLForTesting().GetQuery()); } +TEST_F(OneGoogleBarLoaderImplTest, AsyncParamInjectionIsNeutralized) { + one_google_bar_loader()->SetAdditionalQueryParams( + {{"hl", ""}, {"async", "fixed:0&authuser=1&INJECTED_PARAM=1"}}); + + GURL url = one_google_bar_loader()->GetLoadURLForTesting(); + + std::string value; + EXPECT_FALSE(net::GetValueForKeyInQuery(url, "authuser", &value)); + EXPECT_FALSE(net::GetValueForKeyInQuery(url, "INJECTED_PARAM", &value)); + + EXPECT_EQ("async=fixed:0%26authuser%3D1%26INJECTED_PARAM%3D1&hl=", + url.GetQuery()); +} + +TEST_F(OneGoogleBarLoaderImplTest, AsyncParamPreservesColonAndComma) { + one_google_bar_loader()->SetAdditionalQueryParams( + {{"hl", ""}, {"async", "fixed:0,abp:1"}}); + + EXPECT_EQ("async=fixed:0,abp:1&hl=", + one_google_bar_loader()->GetLoadURLForTesting().GetQuery()); +} + +TEST_F(OneGoogleBarLoaderImplTest, NonAsyncParamColonStaysEscaped) { + one_google_bar_loader()->SetAdditionalQueryParams( + {{"hl", ""}, {"async", "fixed:0"}, {"foo", "a:b"}}); + + EXPECT_EQ("async=fixed:0&foo=a%3Ab&hl=", + one_google_bar_loader()->GetLoadURLForTesting().GetQuery()); +} + TEST_F(OneGoogleBarLoaderImplTest, RequestReturns) { SetUpResponseWithData(kMinimalValidResponse);
Regression Test / PoC
diff --git a/chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl_unittest.cc b/chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl_unittest.cc
index 0c1f88ee..305527e8 100644
--- a/chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl_unittest.cc
+++ b/chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl_unittest.cc
@@ -21,6 +21,7 @@
#include "components/signin/core/browser/signin_header_helper.h"
#include "components/variations/scoped_variations_ids_provider.h"
#include "content/public/test/browser_task_environment.h"
+#include "net/base/url_util.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
@@ -182,6 +183,36 @@
one_google_bar_loader()->GetLoadURLForTesting().GetQuery());
}
+TEST_F(OneGoogleBarLoaderImplTest, AsyncParamInjectionIsNeutralized) {
+ one_google_bar_loader()->SetAdditionalQueryParams(
+ {{"hl", ""}, {"async", "fixed:0&authuser=1&INJECTED_PARAM=1"}});
+
+ GURL url = one_google_bar_loader()->GetLoadURLForTesting();
+
+ std::string value;
+ EXPECT_FALSE(net::GetValueForKeyInQuery(url, "authuser", &value));
+ EXPECT_FALSE(net::GetValueForKeyInQuery(url, "INJECTED_PARAM", &value));
+
+ EXPECT_EQ("async=fixed:0%26authuser%3D1%26INJECTED_PARAM%3D1&hl=",
+ url.GetQuery());
+}
+
+TEST_F(OneGoogleBarLoaderImplTest, AsyncParamPreservesColonAndComma) {
+ one_google_bar_loader()->SetAdditionalQueryParams(
+ {{"hl", ""}, {"async", "fixed:0,abp:1"}});
+
+ EXPECT_EQ("async=fixed:0,abp:1&hl=",
+ one_google_bar_loader()->GetLoadURLForTesting().GetQuery());
+}
+
+TEST_F(OneGoogleBarLoaderImplTest, NonAsyncParamColonStaysEscaped) {
+ one_google_bar_loader()->SetAdditionalQueryParams(
+ {{"hl", ""}, {"async", "fixed:0"}, {"foo", "a:b"}});
+
+ EXPECT_EQ("async=fixed:0&foo=a%3Ab&hl=",
+ one_google_bar_loader()->GetLoadURLForTesting().GetQuery());
+}
+
TEST_F(OneGoogleBarLoaderImplTest, RequestReturns) {
SetUpResponseWithData(kMinimalValidResponse);
Original Bug Report
Query parameter injection in OneGoogleBarLoaderImpl via unescaped async parameter
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential query parameter injection exists in the browser process where the ‘async’ query parameter is raw-concatenated during OneGoogleBar URL construction. A compromised ‘chrome-untrusted://new-tab-page’ renderer could exploit this to force credentialed requests to Google endpoints with attacker-controlled query parameters. The browser returns the resulting response to the renderer, potentially allowing unauthorized data retrieval.
Affected files:
chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl.ccchrome/browser/ui/webui/new_tab_page/untrusted_source.cc
Estimated timestamp from git blame: 2025-03-24
Root Cause Analysis
In OneGoogleBarLoaderImpl::GetApiUrl() (located in chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl.cc), the async query parameter is raw-concatenated to the query string to prevent colons (:) from being percent-encoded. This bypasses the safe net::AppendQueryParameter utility:
// chrome/browser/new_tab_page/one_google_bar/one_google_bar_loader_impl.cc
if (param_pair.first == "async") {
std::string query = api_url.GetQuery() + "&async=" + param_pair.second;
if (query.at(0) == '&') {
query = query.substr(1);
}
GURL::Replacements replacements;
replacements.SetQueryStr(query);
api_url = api_url.ReplaceComponents(replacements);
continue;
}
Because GURL::Replacements::SetQueryStr parses the raw query string, any unescaped & and = characters in param_pair.second survive and are treated as query parameter delimiters. This allows an attacker to inject top-level parameters into the constructed URL.
Data Flow from Untrusted Source
In UntrustedSource::StartDataRequest() (located in chrome/browser/ui/webui/new_tab_page/untrusted_source.cc), the browser extracts query parameters from a subresource request using paramsencoded:
// chrome/browser/ui/webui/new_tab_page/untrusted_source.cc
if (net::GetValueForKeyInQuery(url, "paramsencoded", &query_params)) {
base::Base64Decode(query_params, &query_params);
if (!query_params.empty() && query_params.starts_with("&")) {
params = ExtractQueryParams(query_params.substr(1));
}
}
ExtractQueryParams parses the parameters and percent-decodes their values using url::UrlEscapeDecoder (e.g., converting %26 to & and %3D to =).
If an attacker passes an encoded string containing %26 and %3D, the decoded value in the params map contains literal & and =. When GetApiUrl() processes this map, it raw-concatenates the unescaped value directly into the URL, promoting the injected parameters to top-level URL parameters.
Potential Impact
The browser process dispatches this URL request to Google servers (www.google.com/async/newtab_ogb) using network::mojom::CredentialsMode::kInclude (credentialed with the user’s first-party cookies). Once the request completes, the parsed response is delivered back to the untrusted renderer. A compromised renderer could potentially leverage this to perform credentialed requests with injected parameters (e.g., swapping target sessions via authuser) and read sensitive user data returned by the API.
Note: Our tooling agent does not have the ability to run code dynamically; therefore, these are potential steps and findings that have not been validated with a live proof of concept.
Suggested Potential Reproduction Steps
- Assume a compromised renderer process with code execution capability under the
chrome-untrusted://new-tab-page/origin. - From the renderer console, execute the following request containing the base64-encoded parameter payload:
fetch('chrome-untrusted://new-tab-page/one-google-bar?paramsencoded=' + btoa('&async=fixed:0%26authuser%3D1%26ogdeb%3D1')); - The browser process decodes the
paramsencodedparameter, resolving theasyncvalue tofixed:0&authuser=1&ogdeb=1. - During URL generation, the browser concatenates the raw value, constructing:
https://www.google.com/async/newtab_ogb?hl=<locale>&async=fixed:0&authuser=1&ogdeb=1. - The browser dispatches the credentialed request to the Google endpoint and propagates the sensitive response back to the renderer.
Suggested Fix
To remediate this issue, ensure that any user-controlled input for the async parameter is strictly validated or escaped to prevent delimiter injection. Delimiters such as & and = should not be allowed to be raw-concatenated. Alternatively, parse the sub-components of the async parameter specifically to ensure only colon-separated key-value pairs are allowed, rejecting any input that contains query delimiters.
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.