Skip to content

fix(security): prevent XSS vulnerability in initialize() checkout modal#96

Open
Olayiwolaaa wants to merge 6 commits into
Flutterwave:devfrom
Olayiwolaaa:fix/xss-in-initialize-method
Open

fix(security): prevent XSS vulnerability in initialize() checkout modal#96
Olayiwolaaa wants to merge 6 commits into
Flutterwave:devfrom
Olayiwolaaa:fix/xss-in-initialize-method

Conversation

@Olayiwolaaa

@Olayiwolaaa Olayiwolaaa commented Jun 17, 2026

Copy link
Copy Markdown

Summary

The initialize() method was interpolating user-supplied values directly
into a <script> block without any escaping, allowing XSS attacks via
any of the customer or customization fields.

Problem

Any field passed to the checkout modal (name, email, title, redirect URL,
etc.) could be used to break out of the JS string context. For example:

  • setTitle('</script><script>alert(1)</script>') would inject and execute
    arbitrary JavaScript on the payment page.
  • setRedirectUrl('https://evil.com","public_key":"leaked') could leak
    the public key or redirect users to a phishing page.

Changes

Commit 1 — Quick patch: wrapped all interpolated string values with
addslashes() and cast $this->amount to (float) to close the
immediate injection surfaces.

Commit 2 — Proper fix: replaced the entire string-concatenated JS
block with json_encode() using JSON_HEX_TAG | JSON_HEX_QUOT | JSON_THROW_ON_ERROR, which handles all escaping correctly including
</script> injection that addslashes() does not cover.

Also wired up $this->paymentOptions which was set via setPaymentOptions()
but never actually used in the output — it was hardcoded before.

Verification

Manually verified by setting:

->setTitle('</script><script>alert(1)</script>')

Page source confirmed the payload was safely encoded as:

"title":"\u003C\/script\u003E\u003Cscript\u003Ealert(1)\u003C\/script\u003E"

No alert fired. Modal loaded normally.

Testing

  • tests/Unit/Checkout/InitializeTest.php covers XSS payloads in
    title, customer name, redirect URL, amount casting, and payment options
  • Smoke test available at examples/endpoint/test-initialize.php

Notes

@Abraham-Flutterwave

Abraham-Flutterwave commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Hey @Olayiwolaaa,

Thanks for flagging this! A couple of things before we can move forward:

  1. Could you deprecate the initialize() method and redirect the fix to the Modal getHTML method instead?
  2. Please also retarget this PR against the dev branch rather than master.

Appreciate the contribution!

@Olayiwolaaa Olayiwolaaa changed the base branch from master to dev June 17, 2026 11:19
@gitguardian

gitguardian Bot commented Jun 17, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

Since your pull request originates from a forked repository, GitGuardian is not able to associate the secrets uncovered with secret incidents on your GitGuardian dashboard.
Skipping this check run and merging your pull request will create secret incidents on your GitGuardian dashboard.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
33875719 Triggered Generic High Entropy Secret 6ecf109 src/Monitoring/SignozServiceLogger.php View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@Olayiwolaaa

Copy link
Copy Markdown
Author

Hey @Olayiwolaaa,

Thanks for flagging this! A couple of things before we can move forward:

  1. Could you deprecate the initialize() method and redirect the fix to the Modal getHTML method instead?
  2. Please also retarget this PR against the dev branch rather than master.

Appreciate the contribution!

Hi @Abraham-Flutterwave, thanks for the feedback!

  1. I've moved the json_encode() XSS fix into Modal::getHtml() and
    marked initialize() as deprecated with a trigger_error pointing users
    to render('inline')->with([...])->getHtml() instead.

  2. I've retargeted the PR against dev.

Let me know if anything needs adjusting!

@Abraham-Flutterwave

Abraham-Flutterwave commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Hey @Olayiwolaaa

Please update your test with the ModalEventHandler. see the snippet below:

use Flutterwave\EventHandlers\ModalEventHandler; # add this line 

private function buildInstance(): Flutterwave
    {
        $instance = new Flutterwave();
        $instance
            ->eventHandler(new ModalEventHandler()) # add this line 
            ->setAmount('1000')
            ->setCurrency('NGN')
...

so in src/Flutterwave.php you forgot to use the with($checkoutConfig) before the getHtml() method.

public function initialize(): void
    {
        $checkoutConfig = [
            '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,
            'first_name'        => $this->customerFirstname,
            'last_name'         => $this->customerLastname,
            'email'             => $this->customerEmail,
            'phone_number'      => $this->customerPhone,
            'payment_method'    => $this->paymentOptions,
            'customizations'    => [
                'title'         => $this->customTitle,
                'description'   => $this->customDescription,
                'logo'          => $this->customLogo,
            ],
        ];
        ...

        echo $this->render(Modal::POPUP)->with($checkoutConfig)->getHtml();
  }

Please run the command ./vendor/bin/pest --filter "InitializeTest" to test and ensure backward compatibility of the initialize method.

thanks

@Abraham-Flutterwave

Copy link
Copy Markdown
Contributor

FYA @Olayiwolaaa #96 (comment)

- 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
@Olayiwolaaa

Copy link
Copy Markdown
Author

FYA @Olayiwolaaa #96 (comment)

Hey @Abraham-Flutterwave
Fixes XSS vulnerabilities in the initialize() method by ensuring all
user-supplied values are properly escaped in the rendered checkout modal HTML.

Changes:

src/Flutterwave.php

  • Restructured $checkoutConfig in initialize() to pass fields at the
    top level (email, phone_number, first_name, last_name, payment_method)
    to match the contract expected by Modal::with()
  • Customizations (title, description, logo) now flow through to the modal output

src/Library/Modal.php

  • Added JSON_HEX_APOS flag to escape single quotes in JSON output
  • Added JSON_PRESERVE_ZERO_FRACTION to preserve float formatting (e.g. 100.0)
  • Customization values are set on the payload and included in the checkout config JSON

tests/Unit/Checkout/InitializeTest.php

  • Fixed testInitializeEscapesScriptTagInTitle assertion to not match
    legitimate </script><script> in the HTML template
  • Rewrote testInitializeIsDeprecated to use set_error_handler since
    @trigger_error suppresses PHPUnit's deprecation exception conversion

Testing

All 6 tests in tests/Unit/Checkout/InitializeTest.php pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants