diff --git a/chrome/credential_provider/gaiacp/gaia_credential_base_unittest.cc b/chrome/credential_provider/gaiacp/gaia_credential_base_unittest.cc index 6f1013821d0a2..31baa63675698 100644 --- a/chrome/credential_provider/gaiacp/gaia_credential_base_unittest.cc +++ b/chrome/credential_provider/gaiacp/gaia_credential_base_unittest.cc @@ -891,6 +891,103 @@ INSTANTIATE_TEST_SUITE_P( ::testing::Values(L"", L"acme.com,acme2.com,acme3.com"))); +// ===================================================================== +// SECURITY POC: npos+1 unsigned wrap → domain allowlist bypass. +// +// At gaia_credential_base.cc:2502 the domain is extracted via +// email.substr(email.find(L"@") + 1) +// If a compromised GLS process writes an email that contains NO '@' +// character (e.g. "contoso.com" — exactly an allowed domain, lowercase), +// find() returns npos, npos+1 wraps to 0, and substr(0) returns the whole +// string. The std::ranges::contains() check at :2505 then succeeds. +// +// This test simulates the compromised-GLS scenario via SetGlsEmailAddress +// and asserts the secure behavior. ON UNPATCHED CODE THIS TEST FAILS: +// the logon SUCCEEDS and a new local Windows user is created with the +// attacker-controlled password — that is the vulnerability. +// ===================================================================== +TEST_F(GcpGaiaCredentialBaseTest, SECURITY_NposWrapDomainAllowlistBypass) { + // Enterprise admin policy: only contoso.com accounts may log in. + // (Any functional GCPW deployment has this set; empty allowlist blocks + // all logins per the InvalidDomainTest above.) + ASSERT_EQ(S_OK, SetGlobalFlagForTesting(L"domains_allowed_to_login", + L"contoso.com")); + // permitted_accounts unset → secondary check at :2517 is skipped (default). + ASSERT_EQ(S_OK, SetGlobalFlagForTesting(L"permitted_accounts", L"")); + // Disable cloud association → :549-551 falls straight through to local + // user creation with no network dependency. (With cloud association + // enabled, the chain still completes — see vulnerability report — but + // this is the simplest reproduction.) + ASSERT_EQ(S_OK, SetGlobalFlagForTesting(L"enable_cloud_association", 0)); + + // Baseline: only the gaia runner account exists. + const size_t baseline_users = fake_os_user_manager()->GetUserCount(); + + Microsoft::WRL::ComPtr cred; + ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred)); + Microsoft::WRL::ComPtr test; + ASSERT_EQ(S_OK, cred.As(&test)); + + // ---- Attacker-controlled GLS output ---- + // No '@' in the email field. This is what a compromised GLS browser + // process writes to its stdout pipe (read by LogonUI/SYSTEM). + ASSERT_EQ(S_OK, test->SetGlsEmailAddress("contoso.com")); + // Novel Gaia ID so GetSidFromId/GetSidFromEmail (:497-505) miss. + ASSERT_EQ(S_OK, test->SetGaiaIdOverride( + GaiaId("attacker-novel-id-12345"), + /*ignore_expected_gaia_id=*/true)); + // The password the attacker wants set on the new Windows account. + ASSERT_EQ(S_OK, test->SetGlsGaiaPassword("AttackerP@ss123!")); + + ASSERT_EQ(S_OK, StartLogonProcessAndWait()); + + // SECURE EXPECTED BEHAVIOR: malformed email must be rejected and NO + // local account created. ON UNPATCHED CODE THESE ASSERTIONS FAIL: + // - GetUserCount() returns baseline_users + 1 (NetUserAdd was called) + // - FinishLogonProcess succeeds (logon completes) + // That is the proof of the SYSTEM-privileged account-creation primitive. + EXPECT_EQ(baseline_users, fake_os_user_manager()->GetUserCount()) + << "VULNERABILITY: a local Windows user was created from an " + "@-less email that bypassed the domain allowlist via npos+1 wrap."; + + EXPECT_EQ(S_OK, + FinishLogonProcess( + /*expected_success=*/false, + /*expected_credentials_change_fired=*/false, + GetStringResource(IDS_INVALID_EMAIL_DOMAIN_BASE))) + << "VULNERABILITY: logon succeeded with @-less email matching the " + "allowed domain string."; +} + +// Also assert that a NORMAL out-of-domain email is still blocked, to +// distinguish the npos-wrap bypass from any test-harness misconfiguration. +TEST_F(GcpGaiaCredentialBaseTest, SECURITY_NposWrapControlCase) { + ASSERT_EQ(S_OK, SetGlobalFlagForTesting(L"domains_allowed_to_login", + L"contoso.com")); + ASSERT_EQ(S_OK, SetGlobalFlagForTesting(L"permitted_accounts", L"")); + ASSERT_EQ(S_OK, SetGlobalFlagForTesting(L"enable_cloud_association", 0)); + + Microsoft::WRL::ComPtr cred; + ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred)); + Microsoft::WRL::ComPtr test; + ASSERT_EQ(S_OK, cred.As(&test)); + + // WELL-FORMED email, wrong domain. The allowlist check works correctly + // here because find('@') returns a real index. This test must PASS on + // unpatched code — proving the harness is correct and the failure in + // the test above is genuinely the npos+1 bug. + ASSERT_EQ(S_OK, test->SetGlsEmailAddress("attacker@evil.com")); + ASSERT_EQ(S_OK, test->SetGaiaIdOverride(GaiaId("control-id"), + /*ignore_expected_gaia_id=*/true)); + + ASSERT_EQ(S_OK, StartLogonProcessAndWait()); + ASSERT_EQ(S_OK, + FinishLogonProcess( + /*expected_success=*/false, + /*expected_credentials_change_fired=*/false, + GetStringResource(IDS_INVALID_EMAIL_DOMAIN_BASE))); +} + class GcpGaiaCredentialBasePermittedAccountTest : public GcpGaiaCredentialBaseTest, public ::testing::WithParamInterface<