We were informed of a card-verification scripted/automated exploit against a hosted-fields payment gateway, in which the bad actor utilized the gateway's client-side nonce generator in rapid fashion, then submitting the payment method creation form on Drupal Commerce 2. Their goal was apparently to validate a list of stolen or generated card numbers.

The processor did not particularly object to the nonce generation as nefarious (and there is little mitigation for this, since it's all client side) but we were dinged for not rate-limiting or otherwise restricting the submission of these nonces for payment method creation to the gateway API.

I believe this could be mitigated in a basic way on the server side by implementing flood protection with a sensible default that would not be normally triggered by a human, but will thwart scripted attacks such as this.

Per a quick convo with @mglaman in Slack:

on a call w/ coworker, and did a gut check w/ him who deals with a high fraud customer (not on Commerce...). he think this is the right approach, going for 10 fails per minute or whatever

The Form API doesn't prohibit re-submission by default, and we do need to be able to track failed submissions, so this sounds like a natural job for Flood.

As this potentially affects any on-site payment gateway, I think it's reasonable to implement this at the Commerce core level, rather than per-gateway.

Patch forthcoming.

Issue fork commerce-3111144

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

bradjones1 created an issue. See original summary.

bradjones1’s picture

Status: Active » Needs review
StatusFileSize
new0 bytes
new3.75 KB
bradjones1’s picture

StatusFileSize
new2.27 KB

Oy. Empty test only patch

bradjones1’s picture

StatusFileSize
new5.92 KB

And an updated full patch.

mglaman’s picture

+++ b/modules/payment/src/PluginForm/PaymentMethodAddForm.php
index c6a357b9..a17fe0d8 100644
--- a/modules/payment/src/PluginForm/PaymentMethodFormBase.php

--- a/modules/payment/src/PluginForm/PaymentMethodFormBase.php
+++ b/modules/payment/src/PluginForm/PaymentMethodFormBase.php

Makes sense to do base. I was thinking add only. But someone could pull this off if logged in and trying to add cards to their account.

bradjones1’s picture

Makes sense to do base. I was thinking add only. But someone could pull this off if logged in and trying to add cards to their account.

Also I figured that's where the constructor is, currently, so as to try and minimize the BC break component of this. You can still get in trouble if you're extending and then, like, calling parent:: on a method that accesses the flood service, but AFAIK that's the kind of thing it sounds like this project puts on site owners to navigate around.

The last submitted patch, 3: 3111144-2-test-only.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 4: 3111144-4.patch, failed testing. View results

bradjones1’s picture

Status: Needs work » Needs review
StatusFileSize
new7.09 KB

Tests failing with Behat\Mink\Exception\ElementNotFoundException: Form field with id|name|label|value "payment_method[billing_information][address][0][address][given_name]" not found. - refactoring a bit for repeat submissions.

bradjones1’s picture

StatusFileSize
new6.45 KB

Status: Needs review » Needs work

The last submitted patch, 10: 3111144-10.patch, failed testing. View results

bradjones1’s picture

Status: Needs work » Needs review
StatusFileSize
new7.83 KB
bradjones1’s picture

StatusFileSize
new7.85 KB

Forgot to merge in the optional test data for the submission method. Oy...

The last submitted patch, 12: 3111144-12.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 13: 3111144-13.patch, failed testing. View results

bradjones1’s picture

Status: Needs work » Needs review
StatusFileSize
new8.04 KB

Testbot getting a workout today.

Status: Needs review » Needs work

The last submitted patch, 16: 3111144-16.patch, failed testing. View results

bradjones1’s picture

Status: Needs work » Needs review
StatusFileSize
new8.04 KB

Duh... there's a special decline zip code and I was getting too creative.

Status: Needs review » Needs work

The last submitted patch, 18: 3111144-18.patch, failed testing. View results

bradjones1’s picture

Status: Needs work » Needs review
StatusFileSize
new7.75 KB

Status: Needs review » Needs work

The last submitted patch, 20: 3111144-20.patch, failed testing. View results

bradjones1’s picture

Status: Needs work » Needs review
StatusFileSize
new7.76 KB

So close.

Status: Needs review » Needs work

The last submitted patch, 22: 3111144-22.patch, failed testing. View results

bradjones1’s picture

Status: Needs work » Needs review
StatusFileSize
new7.93 KB

Status: Needs review » Needs work

The last submitted patch, 24: 3111144-24.patch, failed testing. View results

bradjones1’s picture

Status: Needs work » Needs review
StatusFileSize
new8.13 KB

I'm thinking maybe the issue here is with where/how the error on the form is displayed or not? I'm not super well versed in this aspect of the inline/nested form API. Adding a messenger call here akin to the other strings we test on to see if this is related.

Status: Needs review » Needs work

The last submitted patch, 26: 3111144-26.patch, failed testing. View results

bradjones1’s picture

Status: Needs work » Needs review
StatusFileSize
new9.12 KB

Flood's threshold is the number of allowed events - could it be this simple?

Status: Needs review » Needs work

The last submitted patch, 28: 3111144-28.patch, failed testing. View results

bradjones1’s picture

Status: Needs work » Needs review
StatusFileSize
new10.21 KB

Closer... actually triggering the flood now. Also updating some error language on the checkout declines; I *believe* the right message is to say "payment", since a decline at that step could also be related to something other than the payment method itself. Also helps to differentiate between these two (currently identical) messages.

bradjones1’s picture

StatusFileSize
new5.17 KB

Test only patch, now that the whole thing is passing.

bradjones1’s picture

Question on review: Is this sufficient to capture flood on both the one-off payment method add page AND adding a payment method during checkout? I think so, since this is a form that goes inline of the checkout pane?

Status: Needs review » Needs work

The last submitted patch, 31: 3111144-30-test-only.patch, failed testing. View results

bradjones1’s picture

Status: Needs work » Needs review
mglaman’s picture

Question on review: Is this sufficient to capture flood on both the one-off payment method add page AND adding a payment method during checkout? I think so, since this is a form that goes inline of the checkout pane?

Yeah this should capture all the things.

  1. +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php
    @@ -186,13 +186,13 @@ class PaymentProcess extends CheckoutPaneBase {
    -        $message = $this->t('We encountered an error processing your payment method. Please verify your details and try again.');
    +        $message = $this->t('We encountered an error processing your payment. Please verify your details and try again.');
    ...
    -        $message = $this->t('We encountered an unexpected error processing your payment method. Please try again later.');
    +        $message = $this->t('We encountered an unexpected error processing your payment. Please try again later.');
    

    Any specific reason for changing the messaging here? Doing so in this patch would also break translations.

  2. +++ b/modules/payment/src/PluginForm/PaymentMethodFormBase.php
    @@ -51,12 +66,15 @@ class PaymentMethodFormBase extends PaymentGatewayFormBase implements ContainerI
    +  public function __construct(CurrentStoreInterface $current_store, EntityTypeManagerInterface $entity_type_manager, InlineFormManager $inline_form_manager, LoggerInterface $logger, FloodInterface $flood) {
    ...
    +    $this->flood = $flood;
    
    @@ -67,7 +85,8 @@ class PaymentMethodFormBase extends PaymentGatewayFormBase implements ContainerI
    +      $container->get('logger.channel.commerce_payment'),
    +      $container->get('flood')
    

    I'm going to ty and check how many things extend this class in payment gateways. I remember the last time the constructor changed it was a small pain.

    However I can't recall if we consider these constructors as BC.

  3. +++ b/modules/payment/src/PluginForm/PaymentMethodFormBase.php
    @@ -111,7 +130,16 @@ class PaymentMethodFormBase extends PaymentGatewayFormBase implements ContainerI
    +    if (!$this->flood->isAllowed($this->floodKey, 9, 60)) {
    

    Can we document 9 and 60.

  4. +++ b/modules/payment/tests/src/Functional/PaymentMethodTest.php
    @@ -225,12 +216,50 @@ class PaymentMethodTest extends CommerceBrowserTestBase {
    +      'payment_method[payment_details][expiration][year]' => date('Y') + 1,
    

    smort.

mglaman’s picture

Status: Needs review » Needs work

Instead of adding an argument to the constructor, can we use a setFlood method, and the create method does something like

$instance = new  static(...);
$instance->setFlood($container->...);
return $instance;

Preserves BC and headache in payment gateways

rszrama’s picture

Just to confirm, this applies both to standalone payment method creation and embedded payment method creation, as on the checkout form, right? (If so, might want to retitle the issue to make that clear.)

Also, I think we may need to make the flood parameters configurable - 9 attempts in 60 seconds may be a reasonable default, but I can imagine different payment vendors requiring different settings.

mglaman’s picture

Title: Implement flood protection on payment method create form submission » Implement flood protection on payment method forms

So this passes, just needs the nits.

Per #37: this applies anywhere the payment method form is embedded – checkout, user form, any custom form. It's also actually at the base. So anytime a payment method inline form is embedded for create or update it will be utilized.

For a setting.. should it just be Settings::get() or some form UI somewhere? The flood control isn't per payment gateway, it's the system overall. I'm not for a setting because it means building a form for one value and deciding where that form has to live, etc, until there is a demand to expose the setting.

bradjones1’s picture

Status: Needs work » Needs review
Issue tags: +Needs change record

Updated the nullable parameters I tried to use because core only requires PHP 7.0.8; see #3165544: [POLICY] Adopt a minimum PHP 7 version stricter than core for my humble proposal to require Commerce run on at least PHP 7.2. But in the meantime, I'll use an old construct.

The updated MR does a few items which have become required as this is tested in the wild:

  • 60 second window is way too short. The bad guys are smarter than this and will just ramp down the rate to thread the needle. E.g., a 60 second window with a 10 attempt threshold means you could do 9 per minute and still run a card verification attack at a good clip. I've updated the window to 1 day, while also making these options for the flood service configurable as service container parameters. (addresses question in #38.
  • Addressed some more substantive feedback re: the change to the constructor, we can now use a setter for the flood service.

This also probably needs a change record or some sort of opt-in (though I'd rather not go the latter route) since this will increase declines where they are otherwise not, now. Human clients shouldn't have trouble making legitimate payments but this is also IP based and has a 1 day window, so it's worth thinking about.

bradjones1’s picture

StatusFileSize
new12.5 KB

And because GitLab won't let you arbitrarily generate compare diffs between two specific SHAs...

bradjones1’s picture

Addressed nit re: constructor BC. Let me know if this is an appropriate approach.

Proposed CR: https://www.drupal.org/node/3166751

jsacksick’s picture

@bradjones1: re: constructor BC I still would suggest implementing the approach from Matt instead.

This approach is less intrusive and doesn't force all the gateways to tag a new version to get rid of the deprecation notice...