From 0da4773ab62bef202ae62b3dcfa5809a69192837 Mon Sep 17 00:00:00 2001 From: Olayiwolaaa Date: Wed, 17 Jun 2026 05:35:14 +0100 Subject: [PATCH 1/4] fix(security): escape JS string interpolation in initialize() to prevent XSS --- src/Flutterwave.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Flutterwave.php b/src/Flutterwave.php index b62ce7fc..0c307392 100644 --- a/src/Flutterwave.php +++ b/src/Flutterwave.php @@ -345,29 +345,29 @@ public function initialize(): void echo '') 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 0c307392..855e0dcc 100644 --- a/src/Flutterwave.php +++ b/src/Flutterwave.php @@ -334,42 +334,44 @@ public function initialize(): void $this->logger->info('Rendering Payment Modal..'); + $checkoutConfig = json_encode([ + 'public_key' => self::$config->getPublicKey(), + 'tx_ref' => $this->txref, + 'amount' => (float) $this->amount, + 'currency' => $this->currency, + 'country' => $this->country, + 'payment_options' => $this->paymentOptions, + 'redirect_url' => $this->redirectUrl, + 'customer' => [ + 'email' => $this->customerEmail, + 'phone_number' => $this->customerPhone, + 'name' => $this->customerFirstname . ' ' . $this->customerLastname, + ], + 'customizations' => [ + 'title' => $this->customTitle, + 'description' => $this->customDescription, + 'logo' => $this->customLogo, + ], + ], JSON_HEX_TAG | JSON_HEX_QUOT | JSON_THROW_ON_ERROR); + + echo ''; echo ''; + echo ''; echo ''; // $loader_img_src = FLW_PHP_ASSET_DIR."js/v3.js"; echo '
- Proccessing...loading-gif
'; + Processing...loading-gif'; // $script_src = FLW_PHP_ASSET_DIR."js/v3.js"; echo ''; echo ''; echo ''; diff --git a/tests/Unit/Checkout/InitializeTest.php b/tests/Unit/Checkout/InitializeTest.php new file mode 100644 index 00000000..9f991f95 --- /dev/null +++ b/tests/Unit/Checkout/InitializeTest.php @@ -0,0 +1,98 @@ +setAmount('1000') + ->setCurrency('NGN') + ->setCountry('NG') + ->setEmail('test@example.com') + ->setFirstname('John') + ->setLastname('Doe') + ->setPhoneNumber('+2349012345678') + ->setRedirectUrl('https://mysite.com/callback') + ->setTitle('Test Payment') + ->setDescription('Test Description') + ->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(''; + + // echo ''; + // echo ''; + // echo ''; + // $this->logger->info('Rendered Payment Modal Successfully..'); + + @trigger_error( + 'initialize() is deprecated and will be removed in a future version. Use render(\'inline\')->with([...])->getHtml() instead.', + E_USER_DEPRECATED + ); - $this->signoz->trackRequestSent($appId, $environment, 'GET', $this->txref, '/inline'); - - $this->logger->info('Rendering Payment Modal..'); - - $checkoutConfig = json_encode([ - 'public_key' => self::$config->getPublicKey(), - 'tx_ref' => $this->txref, - 'amount' => (float) $this->amount, - 'currency' => $this->currency, - 'country' => $this->country, - 'payment_options' => $this->paymentOptions, - 'redirect_url' => $this->redirectUrl, - 'customer' => [ - 'email' => $this->customerEmail, - 'phone_number' => $this->customerPhone, - 'name' => $this->customerFirstname . ' ' . $this->customerLastname, - ], - 'customizations' => [ - 'title' => $this->customTitle, - 'description' => $this->customDescription, - 'logo' => $this->customLogo, - ], - ], JSON_HEX_TAG | JSON_HEX_QUOT | JSON_THROW_ON_ERROR); - - echo ''; - echo ''; - echo ''; - echo ''; - // $loader_img_src = FLW_PHP_ASSET_DIR."js/v3.js"; - echo '
- Processing...loading-gif
'; - // $script_src = FLW_PHP_ASSET_DIR."js/v3.js"; - echo ''; - - echo ''; - echo ''; - echo ''; - $this->logger->info('Rendered Payment Modal Successfully..'); + $this->createCheckSum(); + echo $this->render(Modal::POPUP)->getHtml(); } /** diff --git a/src/Library/Modal.php b/src/Library/Modal.php index bd90d16c..69a3a9bb 100644 --- a/src/Library/Modal.php +++ b/src/Library/Modal.php @@ -121,36 +121,39 @@ 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'] + ] + ], JSON_HEX_TAG | JSON_HEX_QUOT | JSON_THROW_ON_ERROR); + $html = ''; + $html .= ''; $html .= ''; $html .= ''; $html .= '
- Proccessing...loading-gif
'; + Processing...loading-gif'; $html .= ''; $html .= ''; $html .= ''; From e5bf210fee1282c67d467676cafeffe3718c7b81 Mon Sep 17 00:00:00 2001 From: Olayiwolaaa Date: Wed, 17 Jun 2026 20:27:43 +0100 Subject: [PATCH 4/4] fix: align initialize() payload with Modal::with() expected keys - Move customer fields (email, phone_number, first_name, last_name) to top level in checkout config to match Modal::with() contract - Add payment_method key alongside payment_options - Pass customizations through to Modal via payload setters - Add JSON_HEX_APOS and JSON_PRESERVE_ZERO_FRACTION flags to json_encode in Modal::getHtml() for proper escaping and float output - Fix testInitializeEscapesScriptTagInTitle to not match legitimate HTML template script tags - Rewrite testInitializeIsDeprecated to use set_error_handler since @trigger_error suppresses PHPUnit deprecation conversion --- src/Flutterwave.php | 77 ++++++++------------------ src/Library/Modal.php | 22 ++++++-- tests/Unit/Checkout/InitializeTest.php | 38 ++++++++++--- 3 files changed, 68 insertions(+), 69 deletions(-) diff --git a/src/Flutterwave.php b/src/Flutterwave.php index 0cf7fe9c..65a2a401 100644 --- a/src/Flutterwave.php +++ b/src/Flutterwave.php @@ -326,66 +326,33 @@ public function requeryTransaction(string $referenceNumber): object */ public function initialize(): void { - // $this->createCheckSum(); - - // $appId = $this->signoz->getAppId(); - // $environment = $this->signoz->getCurrentEnvironment(); - - // $this->signoz->trackRequestSent($appId, $environment, 'GET', $this->txref, '/inline'); - - // $this->logger->info('Rendering Payment Modal..'); - - // $checkoutConfig = json_encode([ - // 'public_key' => self::$config->getPublicKey(), - // 'tx_ref' => $this->txref, - // 'amount' => (float) $this->amount, - // 'currency' => $this->currency, - // 'country' => $this->country, - // 'payment_options' => $this->paymentOptions, - // 'redirect_url' => $this->redirectUrl, - // 'customer' => [ - // 'email' => $this->customerEmail, - // 'phone_number' => $this->customerPhone, - // 'name' => $this->customerFirstname . ' ' . $this->customerLastname, - // ], - // 'customizations' => [ - // 'title' => $this->customTitle, - // 'description' => $this->customDescription, - // 'logo' => $this->customLogo, - // ], - // ], JSON_HEX_TAG | JSON_HEX_QUOT | JSON_THROW_ON_ERROR); - - // echo ''; - // echo ''; - // echo ''; - // echo ''; - // // $loader_img_src = FLW_PHP_ASSET_DIR."js/v3.js"; - // echo '
- // Processing...loading-gif
'; - // // $script_src = FLW_PHP_ASSET_DIR."js/v3.js"; - // echo ''; - - // echo ''; - // echo ''; - // echo ''; - // $this->logger->info('Rendered Payment Modal Successfully..'); - @trigger_error( 'initialize() is deprecated and will be removed in a future version. Use render(\'inline\')->with([...])->getHtml() instead.', E_USER_DEPRECATED ); - + $this->createCheckSum(); - echo $this->render(Modal::POPUP)->getHtml(); + + $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, + ], + ]; + + echo $this->render(Modal::POPUP)->with($checkoutConfig)->getHtml(); } /** diff --git a/src/Library/Modal.php b/src/Library/Modal.php index 69a3a9bb..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'], @@ -135,9 +140,14 @@ public function getHtml() 'email' => $payload['email'], 'phone_number' => $payload['phone_number'], 'name' => $payload['fullname'] - ] - ], JSON_HEX_TAG | JSON_HEX_QUOT | JSON_THROW_ON_ERROR); - + ], + '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 .= ''; @@ -147,7 +157,7 @@ public function getHtml() Processing...loading-gif'; $html .= ''; $html .= '', $output); + $this->assertStringContainsString('\u003C', $output); } public function testInitializeEscapesQuotesInCustomerName(): void @@ -93,6 +94,27 @@ public function testInitializeUsesSetPaymentOptions(): void $instance->initialize(); $output = ob_get_clean(); - $this->assertStringContainsString('"payment_options":"card,banktransfer"', $output); + $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