diff --git a/examples/endpoint/test-initialize.php b/examples/endpoint/test-initialize.php new file mode 100644 index 00000000..3ac3ae5c --- /dev/null +++ b/examples/endpoint/test-initialize.php @@ -0,0 +1,47 @@ +setAmount('1000') + ->setCurrency(\Flutterwave\Util\Currency::NGN) + ->setCountry('NG') + ->setEmail('test@example.com') + ->setFirstname('John') + ->setLastname('Doe') + ->setPhoneNumber('+2349067985861') + ->setRedirectUrl("http://{$_SERVER['HTTP_HOST']}/examples/endpoint/verify.php") + ->setTitle('Test Payment') # ->setTitle('') change title to this for testing XSS fix + ->setDescription('Testing initialize XSS fix') + ->setLogo('https://mysite.com/logo.png') + ->setPaymentOptions('card,banktransfer'); + + if (!empty($_REQUEST) && isset($_REQUEST['make'])) { + $flw->initialize(); // this renders the full modal page + exit; + } + +} catch (Exception $e) { + $error = $e->getMessage(); +} +?> + + +
+
+

Initialize() XSS Fix - Smoke Test

+ +
+ +
+
+
+ + + \ No newline at end of file diff --git a/src/Flutterwave.php b/src/Flutterwave.php index b62ce7fc..65a2a401 100644 --- a/src/Flutterwave.php +++ b/src/Flutterwave.php @@ -321,60 +321,38 @@ public function requeryTransaction(string $referenceNumber): object } /** - * Generates the final json to be used in configuring the payment call to the rave payment gateway + * @deprecated Use render('inline')->getHtml() instead. + * Will be removed in a future version. */ public function initialize(): void { + @trigger_error( + 'initialize() is deprecated and will be removed in a future version. Use render(\'inline\')->with([...])->getHtml() instead.', + E_USER_DEPRECATED + ); + $this->createCheckSum(); - $appId = $this->signoz->getAppId(); - $environment = $this->signoz->getCurrentEnvironment(); + $checkoutConfig = [ + 'public_key' => self::$config->getPublicKey(), + 'tx_ref' => $this->txref, + 'amount' => (float) $this->amount, + 'currency' => $this->currency, + 'country' => $this->country, + 'redirect_url' => $this->redirectUrl, + 'payment_method' => $this->paymentOptions, + 'email' => $this->customerEmail, + 'phone_number' => $this->customerPhone, + 'first_name' => $this->customerFirstname, + 'last_name' => $this->customerLastname, + 'customizations' => [ + 'title' => $this->customTitle, + 'description' => $this->customDescription, + 'logo' => $this->customLogo, + ], + ]; - $this->signoz->trackRequestSent($appId, $environment, 'GET', $this->txref, '/inline'); - - $this->logger->info('Rendering Payment Modal..'); - - echo ''; - echo ''; - // $loader_img_src = FLW_PHP_ASSET_DIR."js/v3.js"; - echo '
- Proccessing...loading-gif
'; - // $script_src = FLW_PHP_ASSET_DIR."js/v3.js"; - echo ''; - - echo ''; - echo ''; - echo ''; - $this->logger->info('Rendered Payment Modal Successfully..'); + echo $this->render(Modal::POPUP)->with($checkoutConfig)->getHtml(); } /** diff --git a/src/Library/Modal.php b/src/Library/Modal.php index bd90d16c..adb432b9 100644 --- a/src/Library/Modal.php +++ b/src/Library/Modal.php @@ -70,7 +70,7 @@ public function with(array $args) } $this->customer = (new \Flutterwave\Factories\CustomerFactory())->create($args['customer']); - + $args['customer'] = $this->customer; if (isset($args['tx_ref'])) { @@ -83,10 +83,15 @@ public function with(array $args) } else { $args = array_merge($args, $this->generatedTransactionData); } - $this->payload = (new \Flutterwave\Factories\PayloadFactory())->create($args); + $this->payload = (new \Flutterwave\Factories\PayloadFactory())->create($args); + $this->payload->set('redirect_url', $args['redirect_url']); $this->payload->set('payment_method', $args['payment_method']); + + $this->payload->set('custom_title', $args['customizations']['title'] ?? ''); + $this->payload->set('custom_description', $args['customizations']['description'] ?? ''); + $this->payload->set('custom_logo', $args['customizations']['logo'] ?? ''); $dataToHash = [ 'amount' => $args['amount'], @@ -121,36 +126,44 @@ public function getHtml() $payment_method = $payload['payment_method'] ?? $default_options; $this->logger->info('Rendering Payment Modal..'); + + $checkoutConfig = json_encode([ + 'public_key' => self::$config->getPublicKey(), + 'tx_ref' => $payload['tx_ref'], + 'amount' => $payload['amount'], + 'currency' => $currency, + 'country' => $country, + 'payment_options' => $payment_method, + 'redirect_url' => $payload['redirect_url'], + 'payload_hash' => $payload['payload_hash'], + 'customer' => [ + 'email' => $payload['email'], + 'phone_number' => $payload['phone_number'], + 'name' => $payload['fullname'] + ], + 'customizations' => [ + 'title' => $payload['custom_title'], + 'description' => $payload['custom_description'], + 'logo' => $payload['custom_logo'], + ], + ], JSON_HEX_TAG | JSON_PRESERVE_ZERO_FRACTION | JSON_HEX_QUOT | JSON_HEX_APOS | JSON_THROW_ON_ERROR); + $html = ''; + $html .= ''; $html .= ''; $html .= ''; $html .= '
- Proccessing...loading-gif
'; + Processing...loading-gif'; $html .= ''; $html .= ''; $html .= ''; diff --git a/tests/Unit/Checkout/InitializeTest.php b/tests/Unit/Checkout/InitializeTest.php new file mode 100644 index 00000000..ac3e0159 --- /dev/null +++ b/tests/Unit/Checkout/InitializeTest.php @@ -0,0 +1,120 @@ +eventHandler(new ModalEventHandler()) + ->setAmount('1000') + ->setCurrency(\Flutterwave\Util\Currency::NGN) + ->setCountry('NG') + ->setEmail('test@example.com') + ->setFirstname('John') + ->setLastname('Doe') + ->setPhoneNumber('+2349067985861') + ->setRedirectUrl('https://example.com/callback') + ->setTitle('Test Payment') + ->setDescription('Testing initialize XSS fix') + ->setLogo('https://mysite.com/logo.png') + ->setPaymentOptions('card,banktransfer'); + + return $instance; + } + + public function testInitializeEscapesScriptTagInTitle(): void + { + $instance = $this->buildInstance(); + $instance->setTitle(''); + + ob_start(); + $instance->initialize(); + $output = ob_get_clean(); + + $this->assertStringNotContainsString('', $output); + $this->assertStringContainsString('\u003C', $output); + } + + public function testInitializeEscapesQuotesInCustomerName(): void + { + $instance = $this->buildInstance(); + $instance->setFirstname("O'Brien"); + $instance->setLastname('"Hacker"'); + + ob_start(); + $instance->initialize(); + $output = ob_get_clean(); + + $this->assertStringNotContainsString('"Hacker"', $output); + $this->assertStringNotContainsString("'Brien", $output); + } + + public function testInitializeEscapesRedirectUrl(): void + { + $instance = $this->buildInstance(); + $instance->setRedirectUrl('https://evil.com","public_key":"leaked'); + + ob_start(); + $instance->initialize(); + $output = ob_get_clean(); + + $this->assertStringNotContainsString('"public_key":"leaked', $output); + } + + public function testInitializeCastsAmountToFloat(): void + { + $instance = $this->buildInstance(); + $instance->setAmount('100'); + + ob_start(); + $instance->initialize(); + $output = ob_get_clean(); + + $this->assertStringContainsString('"amount":100.0', $output); + } + + public function testInitializeUsesSetPaymentOptions(): void + { + $instance = $this->buildInstance(); + $instance->setPaymentOptions('card,banktransfer'); + + ob_start(); + $instance->initialize(); + $output = ob_get_clean(); + + $this->assertStringContainsString('card,banktransfer', $output); + } + + public function testInitializeIsDeprecated(): void + { + $instance = $this->buildInstance(); + + $deprecationTriggered = false; + set_error_handler(function (int $errno, string $errstr) use (&$deprecationTriggered) { + if ($errno === E_USER_DEPRECATED && str_contains($errstr, 'initialize() is deprecated')) { + $deprecationTriggered = true; + } + return true; + }); + + ob_start(); + $instance->initialize(); + ob_get_clean(); + + restore_error_handler(); + + $this->assertTrue($deprecationTriggered, 'Expected a deprecation notice for initialize()'); + } +} \ No newline at end of file