diff --git a/components/autofill/core/browser/payments/payments_network_interface_unittest.cc b/components/autofill/core/browser/payments/payments_network_interface_unittest.cc index 77d017acc73f3..f35a8026a83d5 100644 --- a/components/autofill/core/browser/payments/payments_network_interface_unittest.cc +++ b/components/autofill/core/browser/payments/payments_network_interface_unittest.cc @@ -2087,5 +2087,72 @@ TEST_P(PaymentsNetworkInterfaceTestWithPaymentsRpcResultParam, } } +// POC for heap-use-after-free read of +// GetBnplPaymentInstrumentForFetchingUrlRequestDetails::risk_data. +// +// Production scenario: +// 1. BnplManager::FetchRedirectUrl() builds request_details and assigns +// request_details.risk_data = ongoing_flow_state_->risk_data; +// where risk_data is a std::string_view and ongoing_flow_state_->risk_data +// is a heap-backed std::string owned by the per-frame BnplManager. +// 2. PaymentsNetworkInterface (per-WebContents) heap-stores the request and, +// because access_token_ is empty, starts an async OAuth token fetch and +// returns without serializing the body. +// 3. The frame navigates -> BrowserAutofillManager::Reset() -> +// bnpl_manager_.reset() -> ~OngoingFlowState() frees the risk_data buffer. +// ~BnplManager() does NOT call CancelRequest(). +// 4. The OAuth token arrives -> SetOAuth2TokenAndStartRequest() -> +// GetRequestContent() -> BuildRiskDictionary(risk_data) reads the freed +// heap bytes and uploads them to Google Payments. +// +// This test exercises steps 2-4 against the real PaymentsNetworkInterface code +// path; step 1/3 are simulated by a heap unique_ptr, which is the +// exact ownership shape of OngoingFlowState::risk_data inside the heap +// unique_ptr. +TEST_F(PaymentsNetworkInterfaceTest, + GetBnplPaymentInstrumentForFetchingUrl_RiskDataUafOnDeferredOAuth) { + // Heap-allocated owning string: stand-in for + // BnplManager::ongoing_flow_state_->risk_data. 256 bytes guarantees the + // payload lives on the heap (well above libc++ SSO of 22 bytes), matching the + // real base64 device-fingerprint blob. + auto ongoing_flow_state_risk_data = + std::make_unique(256, 'A'); + + GetBnplPaymentInstrumentForFetchingUrlRequestDetails request_details; + request_details.billing_customer_number = 555666777888; + request_details.instrument_id = "INSTRUMENT_ID"; + // Exact analogue of bnpl_manager.cc FetchRedirectUrl(): + // request_details.risk_data = ongoing_flow_state_->risk_data; + // std::string_view binds to the heap buffer of *ongoing_flow_state_risk_data. + request_details.risk_data = *ongoing_flow_state_risk_data; + request_details.merchant_domain = GURL("http://merchant-domain.test/"); + request_details.total_amount = 1000000000; + request_details.currency = "USD"; + + // Step 2: PaymentsNetworkInterfaceBase::IssueRequest() stores the request as + // request_ and, since access_token_ is empty, calls StartTokenFetch(false) + // and returns. GetRequestContent() has NOT been called yet. + payments_network_interface_->GetBnplPaymentInstrumentForFetchingUrl( + std::move(request_details), + base::BindOnce(&PaymentsNetworkInterfaceTest:: + OnDidGetBnplPaymentInstrumentForFetchingUrl, + GetWeakPtr())); + + // Step 3: same-RFH navigation -> ContentAutofillDriverFactory:: + // DidFinishNavigation -> kPendingReset -> BrowserAutofillManager::Reset() -> + // bnpl_manager_.reset(). ~BnplManager() = default, so the in-flight Payments + // request is NOT cancelled. The std::string backing risk_data is freed here. + ongoing_flow_state_risk_data.reset(); + + // Step 4: OAuth token arrives. AccessTokenFetchFinished() -> + // SetOAuth2TokenAndStartRequest() -> request_->GetRequestContent() -> + // BuildRiskDictionary(request_details_.risk_data) reads 256 bytes from the + // freed heap buffer. + // + // ASAN: heap-use-after-free READ in std::string ctor / base::Value::Set, + // freed by std::default_delete above. + IssueOAuthToken(); +} + } // namespace } // namespace autofill::payments