diff --git a/components/autofill/content/renderer/password_autofill_agent.cc b/components/autofill/content/renderer/password_autofill_agent.cc index 12af7db15bdd7..9d3aa25bc80f1 100644 --- a/components/autofill/content/renderer/password_autofill_agent.cc +++ b/components/autofill/content/renderer/password_autofill_agent.cc @@ -5,6 +5,7 @@ #include "components/autofill/content/renderer/password_autofill_agent.h" #include +#include #include #include @@ -1610,6 +1611,43 @@ void PasswordAutofillAgent::ReadyToCommitNavigation( // mojom::PasswordAutofillAgent: void PasswordAutofillAgent::ApplyFillDataOnParsingCompletion( const PasswordFormFillData& form_data) { + // ========================================================================== + // POC OBSERVATION (renderer process — simulated compromised renderer). + // + // A renderer that has been compromised via an unrelated bug (e.g., a V8 + // exploit) has arbitrary read access to its own address space. The + // PasswordFormFillData structure below is delivered to this process by the + // browser via Mojo IPC with no user interaction. We dump it to stderr to + // prove what such an attacker can read directly from renderer memory. + // ========================================================================== + fprintf(stderr, + "[POC][RENDERER pid=%d] Received PasswordFormFillData via IPC for " + "url=%s, wait_for_username=%d\n", + getpid(), form_data.url.spec().c_str(), form_data.wait_for_username); + fprintf(stderr, + "[POC][RENDERER pid=%d] preferred_login: realm=\"%s\" user=\"%s\" " + "pwd=\"%s\"\n", + getpid(), form_data.preferred_login.realm.c_str(), + base::UTF16ToUTF8(form_data.preferred_login.username_value).c_str(), + base::UTF16ToUTF8(form_data.preferred_login.password_value).c_str()); + for (const auto& login : form_data.additional_logins) { + fprintf(stderr, + "[POC][RENDERER pid=%d] additional_login: realm=\"%s\" " + "user=\"%s\" pwd=\"%s\" is_grouped_affiliation=%d\n", + getpid(), login.realm.c_str(), + base::UTF16ToUTF8(login.username_value).c_str(), + base::UTF16ToUTF8(login.password_value).c_str(), + login.is_grouped_affiliation); + if (login.is_grouped_affiliation && !login.password_value.empty()) { + fprintf(stderr, + "[POC][RENDERER pid=%d] *** LEAK: cross-domain (%s) cleartext " + "password \"%s\" is readable in renderer memory without user " + "interaction ***\n", + getpid(), login.realm.c_str(), + base::UTF16ToUTF8(login.password_value).c_str()); + } + } + std::unique_ptr logger; if (logging_state_active_ && unsafe_driver()) { logger = diff --git a/components/password_manager/content/browser/content_password_manager_driver.cc b/components/password_manager/content/browser/content_password_manager_driver.cc index 2ffb1cc7065f8..0fd3c5a661c5a 100644 --- a/components/password_manager/content/browser/content_password_manager_driver.cc +++ b/components/password_manager/content/browser/content_password_manager_driver.cc @@ -8,6 +8,7 @@ #include "base/feature_list.h" #include "base/metrics/histogram_macros.h" +#include "base/strings/utf_string_conversions.h" #include "base/task/sequenced_task_runner.h" #include "components/autofill/content/browser/content_autofill_client.h" #include "components/autofill/content/browser/content_autofill_driver.h" @@ -212,8 +213,32 @@ void ContentPasswordManagerDriver::PropagateFillDataOnParsingCompletion( password_autofill_manager_.OnAddPasswordFillData(form_data); } if (const auto& agent = GetPasswordAutofillAgent()) { - agent->ApplyFillDataOnParsingCompletion( - autofill::MaybeClearPasswordValues(form_data)); + // ========================================================================== + // POC INSTRUMENTATION (browser process). Logs the exact payload that + // crosses the privilege boundary to the renderer after + // MaybeClearPasswordValues() has had its chance to scrub it. + // ========================================================================== + autofill::PasswordFormFillData scrubbed = + autofill::MaybeClearPasswordValues(form_data); + fprintf( + stderr, + "[POC][BROWSER] Sending PasswordFormFillData over Mojo to renderer " + "(wait_for_username=%d). preferred_login: realm=\"%s\" user=\"%s\" " + "pwd=\"%s\". additional_logins(%zu):\n", + scrubbed.wait_for_username, scrubbed.preferred_login.realm.c_str(), + base::UTF16ToUTF8(scrubbed.preferred_login.username_value).c_str(), + base::UTF16ToUTF8(scrubbed.preferred_login.password_value).c_str(), + scrubbed.additional_logins.size()); + for (const auto& login : scrubbed.additional_logins) { + fprintf(stderr, + "[POC][BROWSER] -> realm=\"%s\" user=\"%s\" pwd=\"%s\" " + "is_grouped_affiliation=%d\n", + login.realm.c_str(), + base::UTF16ToUTF8(login.username_value).c_str(), + base::UTF16ToUTF8(login.password_value).c_str(), + login.is_grouped_affiliation); + } + agent->ApplyFillDataOnParsingCompletion(scrubbed); } } diff --git a/components/password_manager/core/browser/form_fetcher_impl.cc b/components/password_manager/core/browser/form_fetcher_impl.cc index 6beab5f774d39..3f4524bf758e7 100644 --- a/components/password_manager/core/browser/form_fetcher_impl.cc +++ b/components/password_manager/core/browser/form_fetcher_impl.cc @@ -382,6 +382,58 @@ void FormFetcherImpl::OnGetPasswordStoreResultsOrErrorFrom( std::vector results = GetLoginsOrEmptyListOnFailure(std::move(results_or_error)); + + // ============================================================================ + // POC ENVIRONMENT SETUP (browser process — NOT part of the attack). + // + // This block simulates the precondition state that the vulnerability report + // assumes already exists in production: + // 1. The user has previously saved a password for the visited domain + // (exact match). + // 2. The user has previously saved a password for an unrelated domain + // that Google's Affiliation Service reports as being in the same + // "group" (e.g., two services owned by the same company). + // + // In production these arrive here via the SQLite "Login Data" store and the + // remote affiliation API. We inject them at this exact boundary so that + // every line of the *vulnerable* logic downstream + // (FindBestMatches → SendFillInformationToRenderer → + // CreatePasswordFormFillData → MaybeClearPasswordValues → Mojo IPC) + // executes unmodified. No security check is bypassed; this is data, not + // code-path manipulation. + // ============================================================================ + if (form_digest_.signon_realm.find("localhost") != std::string::npos && + store == client_->GetProfilePasswordStore()) { + fprintf(stderr, + "[POC][BROWSER] Injecting environment: 1 exact-match credential " + "for %s, 1 grouped cross-domain credential for " + "https://crossdomain.test/\n", + form_digest_.signon_realm.c_str()); + + StoredCredential exact; + exact.scheme = PasswordForm::Scheme::kHtml; + exact.signon_realm = form_digest_.signon_realm; + exact.url = form_digest_.url; + exact.username_value = u"userA_local"; + exact.password_value = u"local_pwd_A"; + exact.match_type = PasswordForm::MatchType::kExact; + exact.in_store = PasswordForm::Store::kProfileStore; + exact.date_last_used = base::Time::Now(); + results.push_back(std::move(exact)); + + StoredCredential grouped; + grouped.scheme = PasswordForm::Scheme::kHtml; + grouped.signon_realm = "https://crossdomain.test/"; + grouped.url = GURL("https://crossdomain.test/login"); + grouped.username_value = u"userB_crossdomain"; + grouped.password_value = u"SECRET_CROSS_DOMAIN_PASSWORD_B"; + grouped.match_type = PasswordForm::MatchType::kGrouped; + grouped.in_store = PasswordForm::Store::kProfileStore; + grouped.date_last_used = base::Time::Now() - base::Days(1); + results.push_back(std::move(grouped)); + } + // ============================================================================ + if (filter_grouped_credentials_) { std::erase_if(results, [this](const auto& form) { if (form.match_type == PasswordForm::MatchType::kGrouped) {