Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions examples/endpoint/test-initialize.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php
require __DIR__."/../../vendor/autoload.php";

session_start();

\Flutterwave\Flutterwave::bootstrap();

try {
$flw = new \Flutterwave\Flutterwave();
$flw->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('</script><script>alert(1)</script>') 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();
}
?>

<link rel="stylesheet" href="../assets/css/index.css">
<div class="buttons">
<form method="get">
<h3>Initialize() XSS Fix - Smoke Test</h3>
<span class="error"><?= $error ?? "" ?></span>
<div class="cta">
<button class="make-payment" name="make" value="1">Test Initialize</button>
</div>
</form>
</div>

<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
crossorigin="anonymous"></script>
<script src="../assets/js/index.js"></script>
74 changes: 26 additions & 48 deletions src/Flutterwave.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<html lang="en">';
echo '<body>';
// $loader_img_src = FLW_PHP_ASSET_DIR."js/v3.js";
echo '<div style="display: flex; flex-direction: row;justify-content: center; align-content: center ">
Proccessing...<img src="../assets/images/ajax-loader.gif" alt="loading-gif"/></div>';
// $script_src = FLW_PHP_ASSET_DIR."js/v3.js";
echo '<script type="text/javascript" src="https://checkout.flutterwave.com/v3.js"></script>';

echo '<script>';
echo 'document.addEventListener("DOMContentLoaded", function(event) {';
echo 'FlutterwaveCheckout({
public_key: "' . self::$config->getPublicKey() . '",
tx_ref: "' . $this->txref . '",
amount: ' . $this->amount . ',
currency: "' . $this->currency . '",
country: "' . $this->country . '",
payment_options: "card,ussd,mpesa,barter,mobilemoneyghana,
mobilemoneyrwanda,mobilemoneyzambia,mobilemoneyuganda,banktransfer,account",
redirect_url:"' . $this->redirectUrl . '",
customer: {
email: "' . $this->customerEmail . '",
phone_number: "' . $this->customerPhone . '",
name: "' . $this->customerFirstname . ' ' . $this->customerLastname . '",
},
callback: function (data) {
console.log(data);
},
onclose: function() {
window.location = "?cancelled=cancelled&cancel_ref=' . $this->txref . '";
},
customizations: {
title: "' . $this->customTitle . '",
description: "' . $this->customDescription . '",
logo: "' . $this->customLogo . '",
}
});';
echo '});';
echo '</script>';
echo '</body>';
echo '</html>';
$this->logger->info('Rendered Payment Modal Successfully..');
echo $this->render(Modal::POPUP)->with($checkoutConfig)->getHtml();
}

/**
Expand Down
61 changes: 37 additions & 24 deletions src/Library/Modal.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'])) {
Expand All @@ -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'],
Expand Down Expand Up @@ -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 .= '<!DOCTYPE html>';
$html .= '<html lang="en">';
$html .= '<body>';
$html .= '<div style="display: flex; flex-direction: row;justify-content: center; align-content: center ">
Proccessing...<img src="../assets/images/ajax-loader.gif" alt="loading-gif"/></div>';
Processing...<img src="../assets/images/ajax-loader.gif" alt="loading-gif"/></div>';
$html .= '<script type="text/javascript" src="https://checkout.flutterwave.com/v3.js"></script>';
$html .= '<script>';
$html .= 'document.addEventListener("DOMContentLoaded", function(event) {';
$html .= 'FlutterwaveCheckout({
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'] . '",
},
callback: function (data) {
console.log(data);
},
onclose: function() {
window.location = "?status=cancelled&tx_ref=' . $payload['tx_ref'] . '";
}
});';
$html .= ' var config = ' . $checkoutConfig . ';';
$html .= ' config.callback = function(data) { console.log(data); };';
$html .= ' config.onclose = function() {';
$html .= ' window.location = "?status=cancelled&tx_ref=' . urlencode($payload['tx_ref']) . '";';
$html .= ' };';
$html .= ' FlutterwaveCheckout(config);';
$html .= '});';
$html .= '</script>';
$html .= '</body>';
Expand Down
120 changes: 120 additions & 0 deletions tests/Unit/Checkout/InitializeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

namespace Unit\Checkout;

use PHPUnit\Framework\TestCase;
use Flutterwave\Flutterwave;
use Flutterwave\EventHandlers\ModalEventHandler;

class InitializeTest extends TestCase
{
protected function setUp(): void
{
Flutterwave::bootstrap();
}

private function buildInstance(): Flutterwave
{
$instance = new Flutterwave();
$instance
->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('</script><script>alert(1)</script>');

ob_start();
$instance->initialize();
$output = ob_get_clean();

$this->assertStringNotContainsString('<script>alert(1)</script>', $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()');
}
}