Chrome · SignIn › Signin
CVE-2026-113114
UAF in SignIn (Signin)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/extensions/api/identity/identity_get_auth_token_function.cc |
modified |
Files Changed
chrome/browser/extensions/api/identity/identity_apitest.ccchrome/browser/extensions/api/identity/identity_get_auth_token_function.hchrome/browser/extensions/api/identity/identity_get_auth_token_function.cc
Patch
From 3d260133feb022041965feb6df42a232cd8323e1 Mon Sep 17 00:00:00 2001
From: Ernest Nguyen Hung <ernn@google.com>
Date: Tue, 15 Sep 2026 04:15:16 -0700
Subject: [PATCH] [signin] Defer persisting account until `OnMintTokenSuccess`
This CL defers calling `SetGaiaIdForExtension` until
`OnMintTokenSuccess` for `getAuthToken` extension api, guaranteeing that
the extension's account binding is only written to `ExtensionPrefs`
after the server successfully validates the consent result.
Fixed: 497087197
Change-Id: I1eafb042ad1a2958254f6bbeae6ee03159937489
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8381041
Reviewed-by: Alex Ilin <alexilin@chromium.org>
Commit-Queue: Ernest Nguyen Hung <ernn@google.com>
Cr-Commit-Position: refs/heads/main@{#1697979}
---
diff --git a/chrome/browser/extensions/api/identity/identity_get_auth_token_function.cc b/chrome/browser/extensions/api/identity/identity_get_auth_token_function.cc
index c4af70f..b37a00e5 100644
--- a/chrome/browser/extensions/api/identity/identity_get_auth_token_function.cc
+++ b/chrome/browser/extensions/api/identity/identity_get_auth_token_function.cc
@@ -698,10 +698,15 @@
IdentityTokenCacheValue token = IdentityTokenCacheValue::CreateToken(
result.access_token, result.granted_scopes, result.time_to_live);
- IdentityAPI::GetFactoryInstance()
- ->Get(GetProfile())
- ->token_cache()
- ->SetToken(token_key_, token);
+ IdentityAPI* id_api = IdentityAPI::GetFactoryInstance()->Get(GetProfile());
+
+ id_api->token_cache()->SetToken(token_key_, token);
+ // Persist the account once the remote consent flow has been verified by a
+ // successful mint token response.
+ if (remote_consent_approved_) {
+ id_api->SetGaiaIdForExtension(token_key_.extension_id,
+ token_key_.account_info.gaia);
+ }
CompleteMintTokenFlow();
CompleteFunctionWithResult(result.access_token, result.granted_scopes);
@@ -892,16 +897,16 @@
}
}
- IdentityAPI* id_api = IdentityAPI::GetFactoryInstance()->Get(GetProfile());
- id_api->SetGaiaIdForExtension(token_key_.extension_id, gaia_id);
-
// It's important to update the cache before calling CompleteMintTokenFlow()
// as this call may start a new request synchronously and query the cache.
ExtensionTokenKey new_token_key(token_key_);
new_token_key.account_info = account.GetCoreAccountInfo();
- id_api->token_cache()->SetToken(
- new_token_key,
- IdentityTokenCacheValue::CreateRemoteConsentApproved(consent_result));
+ IdentityAPI::GetFactoryInstance()
+ ->Get(GetProfile())
+ ->token_cache()
+ ->SetToken(
+ new_token_key,
+ IdentityTokenCacheValue::CreateRemoteConsentApproved(consent_result));
CompleteMintTokenFlow();
token_key_ = new_token_key;
consent_result_ = consent_result;
diff --git a/chrome/browser/extensions/api/identity/identity_get_auth_token_function.h b/chrome/browser/extensions/api/identity/identity_get_auth_token_function.h
index 53bdc5b2..dec25637 100644
--- a/chrome/browser/extensions/api/identity/identity_get_auth_token_function.h
+++ b/chrome/browser/extensions/api/identity/identity_get_auth_token_function.h
@@ -278,7 +278,10 @@
#endif
std::unique_ptr<GaiaRemoteConsentFlow> gaia_remote_consent_flow_;
std::string consent_result_;
- // Added for debugging https://crbug.com/40134189.
+ // Indicates whether remote consent was approved for this request.
+ // Used for UMA logging and to gate persisting the account binding in
+ // `OnMintTokenSuccess`. See https://crbug.com/40134189 and
+ // https://crbug.com/497087197.
bool remote_consent_approved_ = false;
// Invoked when IdentityAPI is shut down.
diff --git a/chrome/browser/extensions/api/identity/identity_apitest.cc b/chrome/browser/extensions/api/identity/identity_apitest.cc
index ed66eaf..c35f503 100644
--- a/chrome/browser/extensions/api/identity/identity_apitest.cc
+++ b/chrome/browser/extensions/api/identity/identity_apitest.cc
@@ -148,6 +148,7 @@
using ::testing::_;
using ::testing::HasSubstr;
using ::testing::Return;
+using ::testing::StartsWith;
namespace extensions {
@@ -2969,6 +2970,78 @@
}
}
+// Regression test for https://crbug.com/497087197. Tests that when remote
+// consent flow is approved but the subsequent mint token flow fails (e.g.,
+// server rejects an invalid or forged consent result), Chrome does NOT persist
+// the account to prefs. Chrome should continue using the primary account in
+// subsequent `getAuthToken` calls for that extension.
+IN_PROC_BROWSER_TEST_F(
+ GetAuthTokenFunctionTest,
+ MultiSecondaryInteractiveRemoteConsentMintTokenFailureDoesNotPersistAccount) {
+ if (id_api()->AreExtensionsRestrictedToPrimaryAccount()) {
+ GTEST_SKIP() << "Extensions are restricted to the primary account.";
+ }
+
+ const CoreAccountId primary_account_id = SignIn("primary@example.com");
+ const AccountInfo secondary_account =
+ identity_test_env()->MakeAccountAvailable("secondary@example.com");
+ const extensions::Extension* extension = CreateExtension(CLIENT_ID | SCOPES);
+
+ {
+ auto func = base::MakeRefCounted<FakeGetAuthTokenFunction>();
+ func->set_extension(extension);
+ func->push_mint_token_result(
+ TestOAuth2MintTokenFlow::REMOTE_CONSENT_SUCCESS);
+ func->push_mint_token_result(TestOAuth2MintTokenFlow::MINT_TOKEN_FAILURE);
+ func->set_remote_consent_gaia_id(secondary_account.GetGaiaId());
+ func->set_auto_login_access_token(false);
+
+ base::RunLoop run_loop;
+ on_access_token_requested_ = run_loop.QuitClosure();
+ RunFunctionAsync(func.get(), /*args=*/"[{\"interactive\": true}]");
+ run_loop.Run();
+
+ IssueLoginAccessTokenForAccount(primary_account_id);
+ IssueLoginAccessTokenForAccount(secondary_account.GetAccountId());
+
+ ASSERT_THAT(WaitForError(func.get()), StartsWith(errors::kAuthFailure));
+ // The unverified account must NOT be persisted in prefs.
+ ASSERT_EQ(id_api()->GetGaiaIdForExtension(extension->id()), std::nullopt);
+ histogram_tester()->ExpectUniqueSample(
+ kGetAuthTokenResultHistogramName,
+ IdentityGetAuthTokenError::State::kMintTokenAuthFailure, 1);
+ histogram_tester()->ExpectUniqueSample(
+ kGetAuthTokenResultAfterConsentApprovedHistogramName,
+ IdentityGetAuthTokenError::State::kMintTokenAuthFailure, 1);
+ }
+
+ {
+ // Clear in-memory token cache to simulate a browser restart.
+ id_api()->token_cache()->EraseAllTokens();
+
+ // A subsequent `getAuthToken` call without an account parameter should
+ // fall back to the primary account rather than the secondary account.
+ auto func = base::MakeRefCounted<FakeGetAuthTokenFunction>();
+ func->set_extension(extension);
+ func->push_mint_token_result(TestOAuth2MintTokenFlow::MINT_TOKEN_SUCCESS);
+
+ std::string access_token;
+ std::set<std::string> granted_scopes;
+ RunGetAuthTokenFunction(func.get(), /*args=*/"[{}]", profile(),
+ &access_token, &granted_scopes);
+ EXPECT_EQ(access_token, kAccessToken);
+ EXPECT_EQ(func->GetExtensionTokenKeyForTest()->account_info.account_id,
+ primary_account_id);
+ EXPECT_EQ(id_api()->GetGaiaIdForExtension(extension->id()), std::nullopt);
+ histogram_tester()->ExpectBucketCount(
+ kGetAuthTokenResultHistogramName,
+ IdentityGetAuthTokenError::State::kNone, 1);
+ histogram_tester()->ExpectBucketCount(
+ kGetAuthTokenResultAfterConsentApprovedHistogramName,
+ IdentityGetAuthTokenError::State::kNone, 0);
+ }
+}
+
// Tests two concurrent remote consent flows. Both of them should succeed.
// The second flow starts while the first one is blocked on an interactive mint
// token flow. This is a regression test for https://crbug.com/40134189.
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/extensions/api/identity/identity_apitest.cc b/chrome/browser/extensions/api/identity/identity_apitest.cc
index ed66eaf..c35f503 100644
--- a/chrome/browser/extensions/api/identity/identity_apitest.cc
+++ b/chrome/browser/extensions/api/identity/identity_apitest.cc
@@ -148,6 +148,7 @@
using ::testing::_;
using ::testing::HasSubstr;
using ::testing::Return;
+using ::testing::StartsWith;
namespace extensions {
@@ -2969,6 +2970,78 @@
}
}
+// Regression test for https://crbug.com/497087197. Tests that when remote
+// consent flow is approved but the subsequent mint token flow fails (e.g.,
+// server rejects an invalid or forged consent result), Chrome does NOT persist
+// the account to prefs. Chrome should continue using the primary account in
+// subsequent `getAuthToken` calls for that extension.
+IN_PROC_BROWSER_TEST_F(
+ GetAuthTokenFunctionTest,
+ MultiSecondaryInteractiveRemoteConsentMintTokenFailureDoesNotPersistAccount) {
+ if (id_api()->AreExtensionsRestrictedToPrimaryAccount()) {
+ GTEST_SKIP() << "Extensions are restricted to the primary account.";
+ }
+
+ const CoreAccountId primary_account_id = SignIn("primary@example.com");
+ const AccountInfo secondary_account =
+ identity_test_env()->MakeAccountAvailable("secondary@example.com");
+ const extensions::Extension* extension = CreateExtension(CLIENT_ID | SCOPES);
+
+ {
+ auto func = base::MakeRefCounted<FakeGetAuthTokenFunction>();
+ func->set_extension(extension);
+ func->push_mint_token_result(
+ TestOAuth2MintTokenFlow::REMOTE_CONSENT_SUCCESS);
+ func->push_mint_token_result(TestOAuth2MintTokenFlow::MINT_TOKEN_FAILURE);
+ func->set_remote_consent_gaia_id(secondary_account.GetGaiaId());
+ func->set_auto_login_access_token(false);
+
+ base::RunLoop run_loop;
+ on_access_token_requested_ = run_loop.QuitClosure();
+ RunFunctionAsync(func.get(), /*args=*/"[{\"interactive\": true}]");
+ run_loop.Run();
+
+ IssueLoginAccessTokenForAccount(primary_account_id);
+ IssueLoginAccessTokenForAccount(secondary_account.GetAccountId());
+
+ ASSERT_THAT(WaitForError(func.get()), StartsWith(errors::kAuthFailure));
+ // The unverified account must NOT be persisted in prefs.
+ ASSERT_EQ(id_api()->GetGaiaIdForExtension(extension->id()), std::nullopt);
+ histogram_tester()->ExpectUniqueSample(
+ kGetAuthTokenResultHistogramName,
+ IdentityGetAuthTokenError::State::kMintTokenAuthFailure, 1);
+ histogram_tester()->ExpectUniqueSample(
+ kGetAuthTokenResultAfterConsentApprovedHistogramName,
+ IdentityGetAuthTokenError::State::kMintTokenAuthFailure, 1);
+ }
+
+ {
+ // Clear in-memory token cache to simulate a browser restart.
+ id_api()->token_cache()->EraseAllTokens();
+
+ // A subsequent `getAuthToken` call without an account parameter should
+ // fall back to the primary account rather than the secondary account.
+ auto func = base::MakeRefCounted<FakeGetAuthTokenFunction>();
+ func->set_extension(extension);
+ func->push_mint_token_result(TestOAuth2MintTokenFlow::MINT_TOKEN_SUCCESS);
+
+ std::string access_token;
+ std::set<std::string> granted_scopes;
+ RunGetAuthTokenFunction(func.get(), /*args=*/"[{}]", profile(),
+ &access_token, &granted_scopes);
+ EXPECT_EQ(access_token, kAccessToken);
+ EXPECT_EQ(func->GetExtensionTokenKeyForTest()->account_info.account_id,
+ primary_account_id);
+ EXPECT_EQ(id_api()->GetGaiaIdForExtension(extension->id()), std::nullopt);
+ histogram_tester()->ExpectBucketCount(
+ kGetAuthTokenResultHistogramName,
+ IdentityGetAuthTokenError::State::kNone, 1);
+ histogram_tester()->ExpectBucketCount(
+ kGetAuthTokenResultAfterConsentApprovedHistogramName,
+ IdentityGetAuthTokenError::State::kNone, 0);
+ }
+}
+
// Tests two concurrent remote consent flows. Both of them should succeed.
// The second flow starts while the first one is blocked on an interactive mint
// token flow. This is a regression test for https://crbug.com/40134189.
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.
References
On This Page