Skip to content
Snippets Groups Projects
Commit e226f372 authored by Rich Lott / Artful Robot's avatar Rich Lott / Artful Robot
Browse files

Replace Firewall with Moat

moat: update info

moat: Implement civi.moat.getimplementations handler

Move to event method for generating csrf

Fix typo in csrf check

Update /CRM/Stripe/PaymentIntent.php

Update /CRM/Stripe/PaymentIntent.php

Replace Firewall with moat (2)

Add MIN_VERSION_MOAT

remove createorupdate (Matt did this, I don't use it)
parent 33d3c8ba
Branches artfulrobot-moat-6.10
No related tags found
No related merge requests found
......@@ -483,6 +483,15 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
$jsVars['csrfToken'] = $firewall->generateCSRFToken($context);
}
// Generate csrf token valid for 5 mins.
$event = Civi\Core\Event\GenericHookEvent::create([
'createdToken' => NULL,
'validTo' => 5,
'data' => $context ?? NULL,
]);
Civi::dispatcher()->dispatch('civi.moat.token', $event);
$jsVars['csrfToken'] = $event->createdToken;
// Add CSS via region (it won't load on drupal webform if added via \Civi::resources()->addStyleFile)
CRM_Core_Region::instance('billing-block')->add([
'styleUrl' => \Civi::service('asset_builder')->getUrl(
......
......@@ -27,6 +27,7 @@ class CRM_Stripe_Check {
*/
const MIN_VERSION_MJWSHARED = '1.2.14';
const MIN_VERSION_FIREWALL = '1.5.8';
const MIN_VERSION_MOAT = '1.1';
/**
* @var array
......@@ -48,7 +49,7 @@ class CRM_Stripe_Check {
*/
public function checkRequirements() {
$this->checkExtensionMjwshared();
$this->checkExtensionFirewall();
$this->checkExtensionMoat();
$this->checkWebhooks();
$this->checkFailedPaymentIntents();
return $this->messages;
......@@ -138,8 +139,8 @@ class CRM_Stripe_Check {
/**
* @throws \CiviCRM_API3_Exception
*/
private function checkExtensionFirewall() {
$extensionName = 'firewall';
private function checkExtensionMoat() {
$extensionName = 'moat';
$extensions = civicrm_api3('Extension', 'get', [
'full_name' => $extensionName,
......@@ -148,9 +149,9 @@ class CRM_Stripe_Check {
if (empty($extensions['count']) || ($extensions['values'][$extensions['id']]['status'] !== 'installed')) {
$message = new CRM_Utils_Check_Message(
__FUNCTION__ . 'stripe_recommended',
E::ts('If you are using Stripe to accept payments on public forms (eg. contribution/event registration forms) it is required that you install the <strong><a href="https://lab.civicrm.org/extensions/firewall">firewall</a></strong> extension.
E::ts('If you are using Stripe to accept payments on public forms (eg. contribution/event registration forms) it is required that you install the <strong><a href="https://lab.civicrm.org/extensions/moat">moat</a></strong> extension.
Some sites have become targets for spammers who use the payment endpoint to try and test credit cards by submitting invalid payments to your Stripe account.'),
E::ts('Required Extension: firewall'),
E::ts('Required Extension: Moat'),
\Psr\Log\LogLevel::ERROR,
'fa-lightbulb-o'
);
......@@ -163,7 +164,7 @@ class CRM_Stripe_Check {
$this->messages[] = $message;
}
if (isset($extensions['id']) && $extensions['values'][$extensions['id']]['status'] === 'installed') {
$this->requireExtensionMinVersion($extensionName, CRM_Stripe_Check::MIN_VERSION_FIREWALL, $extensions['values'][$extensions['id']]['version']);
$this->requireExtensionMinVersion($extensionName, CRM_Stripe_Check::MIN_VERSION_MOAT, $extensions['values'][$extensions['id']]['version']);
}
}
......
......@@ -391,24 +391,21 @@ class CRM_Stripe_PaymentIntent {
*/
$resultObject = (object) ['ok' => FALSE, 'message' => '', 'data' => []];
if (class_exists('\Civi\Firewall\Event\FraudEvent')) {
if (!empty($this->extraData)) {
// The firewall will block IP addresses when it detects fraud.
// This additionally checks if the same details are being used on a different IP address.
$ipAddress = \Civi\Firewall\Firewall::getIPAddress();
// Where a payment is declined as likely fraud, log it as a more serious exception
$numberOfFailedAttempts = \Civi\Api4\StripePaymentintent::get(FALSE)
->selectRowCount()
->addWhere('extra_data', '=', $this->extraData)
->addWhere('status', '=', 'failed')
->addWhere('created_date', '>', '-2 hours')
->execute()
->count();
if ($numberOfFailedAttempts > 5) {
\Civi\Firewall\Event\FraudEvent::trigger($ipAddress, CRM_Utils_String::ellipsify('StripeProcessPaymentIntent: ' . $this->extraData, 255));
}
}
// Control floods of intent processing
$event = Civi\Core\Event\GenericHookEvent::create([
'action' => 'stripe_process_payment_intent',
'identifiers' => ['ip'],
'ok' => TRUE,
]);
if (!empty($this->extraData)) {
// Enable us to ensure (in moat config) that same intent is not tried twice.
$event->identifiers[] = "stripe_intent_id:$this->extraData";
}
Civi::dispatcher()->dispatch('civi.moat.drip', $event);
if (!$event->ok) {
// A flood was triggered, but the moat config didn't cause a die
// (it probably should have), so exit now gracefully.
return $resultObject;
}
$intentParams = [];
......@@ -472,46 +469,30 @@ class CRM_Stripe_PaymentIntent {
if ($e instanceof \Stripe\Exception\CardException) {
// Determine likely fraud
$fraud = FALSE;
if (method_exists('\Civi\Firewall\Firewall', 'getIPAddress')) {
$ipAddress = \Civi\Firewall\Firewall::getIPAddress();
if ($e->getDeclineCode() === 'fraudulent') {
$fraud = TRUE;
}
else {
$ipAddress = \CRM_Utils_System::ipAddress();
}
// Where a payment is declined as likely fraud, log it as a more serious exception
if (class_exists('\Civi\Firewall\Event\FraudEvent')) {
// Fraud response from issuer
if ($e->getDeclineCode() === 'fraudulent') {
$fraud = TRUE;
}
// Look for fraud detected by Stripe Radar
else {
$jsonBody = $e->getJsonBody();
if (!empty($jsonBody['error']['payment_intent']['charges']['data'])) {
foreach ($jsonBody['error']['payment_intent']['charges']['data'] as $charge) {
if ($charge['outcome']['type'] === 'blocked') {
$fraud = TRUE;
break;
}
$jsonBody = $e->getJsonBody();
if (!empty($jsonBody['error']['payment_intent']['charges']['data'])) {
foreach ($jsonBody['error']['payment_intent']['charges']['data'] as $charge) {
if ($charge['outcome']['type'] === 'blocked') {
$fraud = TRUE;
break;
}
}
}
if ($fraud) {
\Civi\Firewall\Event\FraudEvent::trigger($ipAddress, 'CRM_Stripe_PaymentIntent::processPaymentIntent');
}
}
// Multiple declined card attempts is an indicator of card testing
if (!$fraud && class_exists('\Civi\Firewall\Event\DeclinedCardEvent')) {
\Civi\Firewall\Event\DeclinedCardEvent::trigger($ipAddress, 'CRM_Stripe_PaymentIntent::processPaymentIntent');
}
// Allow configurable numbers of decline and fraud.
$event = Civi\Core\Event\GenericHookEvent::create([
'action' => $fraud ? 'stripe_likely_fraud' : 'stripe_decline',
'identifiers' => ['ip'],
'ok' => TRUE,
]);
Civi::dispatcher()->dispatch('civi.moat.drip', $event);
// Returned message should not indicate whether fraud was detected
$message = $parsedError['message'];
......
......@@ -9,7 +9,6 @@
+--------------------------------------------------------------------+
*/
use Civi\Firewall\Firewall;
use CRM_Stripe_ExtensionUtil as E;
/**
......
......@@ -26,7 +26,7 @@ There are a number of planned features that depend on funding/resources. See [Ro
#### Recommended extensions
* [Firewall extension](https://civicrm.org/extensions/firewall).
* [Moat extension](https://codeberg.org/artfulrobot/moat).
* [contributiontransactlegacy extension](https://civicrm.org/extensions/contribution-transact-api) if using drupal 7 webform.
**Please ensure that you are running the "Stripe: Cleanup" scheduled job every hour or you will have issues with failed/uncaptured payments appearing on customer credit cards and blocking their balance for up to a week!**
......
# Moat configuration
Moat is a dynamic firewall-like API. When implemented by extensions (e.g. this
one), it allows site admins to say how much of what should be allowed to occur.
This extension suggests the following configuration for a 'normal' alert level.
## Event: `stripe_process_payment_intent`
What's too many attempts to process a payment intent per IP? Suggest: 3/hour and 9/week, action: die, block
What's too many attempts to process a particular payment intent? Suggest: 1/year, action: die
## Event: `stripe_likely_fraud`
How much likely fraud is too much per IP? 1/week and 2/month, action: die, block
## Event: `stripe_decline`
How many declines per IP? Declines are normal, but too many indicates card testing. How about 6/hour and 10/month, action: die, block
## Higher alert levels
Moat can be configured to move up to a heightened alert level in certain situations. You may want to provide tighter rules for these situations.
......@@ -9,6 +9,10 @@ Releases use the following numbering system:
* **[BC]**: Items marked with [BC] indicate a breaking change that will require updates to your code if you are using that code in your extension.
## artfulrobot-moat fork
* Removes Firewall, implements Moat.
## Release 6.10 (2023-10-10)
**Supports Stripe API version 2023-08-16 (and will force it to be used).**
......@@ -31,7 +35,6 @@ Update tests to use StripePaymentintent.ProcessPublic instead of (deleted) API3
* Support (and require) API version 2023-08-16.
* Update stripe-php library from 9 to 12.
## Release 6.9.4 (2023-09-22)
**Stripe API version 2023-08-16 is NOT supported.
The last supported API version for 6.9.x is 2022-11-15 See [#446](https://lab.civicrm.org/extensions/stripe/-/issues/446)**
......
<?xml version="1.0"?>
<extension key="com.drastikbydesign.stripe" type="module">
<file>stripe</file>
<name>Stripe Payment Processor</name>
<name>Stripe Payment Processor (Moat fork)</name>
<description>Accept payments using https://stripe.com/</description>
<urls>
<url desc="Main Extension Page">https://lab.civicrm.org/extensions/stripe</url>
......@@ -21,13 +21,15 @@
<compatibility>
<ver>5.64</ver>
</compatibility>
<comments>Original Author: Joshua Walker (drastik) - Drastik by Design.
<comments>
This is the Moat fork by Rich Lott / Artful Robot. Original Author: Joshua
Walker (drastik) - Drastik by Design.
Jamie Mcclelland (ProgressiveTech) did a lot of the 5.x compatibility work.
Stripe generously donates a portion of each transaction to support CiviCRM.
</comments>
<requires>
<ext>mjwshared</ext>
<ext>firewall</ext>
<ext>moat</ext>
</requires>
<civix>
<namespace>CRM/Stripe</namespace>
......
......@@ -22,6 +22,13 @@ use CRM_Stripe_ExtensionUtil as E;
*/
function stripe_civicrm_config(&$config) {
_stripe_civix_civicrm_config($config);
if (!isset(Civi::$statics[__FUNCTION__])) {
Civi::$statics[__FUNCTION__] = TRUE;
// Register hook/event listeners here
$d = Civi::dispatcher();
$d->addListener('civi.moat.getimplementations', 'stripe_handleGetMoatImplementations');
}
}
/**
......@@ -179,3 +186,50 @@ function stripe_civicrm_shutdown_updatestripecustomer(int $contactID) {
}
}
function stripe_handleGetMoatImplementations(\Civi\Core\Event\GenericHookEvent $event) {
$event->actions['stripe'] = [
'stripe_process_payment_intent' => [
'description' => 'This fires when a payment intent is being processed. up to 3/ip/hour, up to 10/ip/week is probably ok. But each intent must only be processed once.',
'suggestedConfig' => [
'ip4:' => [[
'rates' => [
['limit' => 3, 'minutes' => 60],
['limit' => 10, 'minutes' => 60*24*7]
],
'flood' => [ 'block' => TRUE, 'die' => 400 ]
]],
'stripe_intent_id:' => [[
'rates' => [['limit' => 2, 'minutes' => 60*24*7]],
'flood' => [ 'block' => TRUE, 'die' => 400 ]
]],
]
],
'stripe_likely_fraud' => [
'description' => 'A payment was declined and it looks likely to have been for fraud reasons. Allow 1 max per day per ip, 2 max per ip per week.',
'suggestedConfig' => [
'ip4:' => [[
'rates' => [['limit' => 2, 'minutes' => 60*24], ['limit' => 3, 'minutes' => 60*24*7]],
'flood' => [ 'block' => TRUE, 'die' => 400 ]
]],
]
],
'stripe_decline' => [
'description' => 'A payment was declined on some basis, likely non-fraudulent. Limits of 10/day, 20/week.',
'suggestedConfig' => [
'ip4:' => [[
'rates' => [['limit' => 10, 'minutes' => 60*24], ['limit' => 20, 'minutes' => 60*24*7]],
'flood' => [ 'block' => TRUE, 'die' => 400 ]
]],
]
],
];
$event->identifierPrefixes['stripe'] = [
'stripe_intent_id:' => [
'description' => 'A particular payment intent',
]
];
}
......@@ -68,14 +68,14 @@ abstract class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implement
if (!is_dir(__DIR__ . '/../../../../../mjwshared')) {
civicrm_api3('Extension', 'download', ['key' => 'mjwshared']);
}
if (!is_dir(__DIR__ . '/../../../../../firewall')) {
civicrm_api3('Extension', 'download', ['key' => 'firewall']);
if (!is_dir(__DIR__ . '/../../../../../moat')) {
civicrm_api3('Extension', 'download', ['key' => 'moat']);
}
return \Civi\Test::headless()
->installMe(__DIR__)
->install('mjwshared')
->install('firewall')
->install('moat')
->apply($reInstall);
}
......@@ -391,7 +391,6 @@ abstract class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implement
$paymentIntentID = NULL;
$paymentMethodID = NULL;
$firewall = new \Civi\Firewall\Firewall();
if (!isset($params['is_recur'])) {
// Send in payment method to get payment intent.
$paymentIntentParams = [
......@@ -400,7 +399,7 @@ abstract class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implement
'payment_processor_id' => $this->paymentProcessorID,
'payment_intent_id' => $params['paymentIntentID'] ?? NULL,
'description' => NULL,
'csrfToken' => $firewall->generateCSRFToken(),
'csrfToken' => $this->generateCSRFToken(),
];
$result = \Civi\Api4\StripePaymentintent::processPublic(TRUE)
->setPaymentMethodID($paymentMethod->id)
......@@ -468,6 +467,23 @@ abstract class CRM_Stripe_BaseTest extends \PHPUnit\Framework\TestCase implement
return $ret;
}
protected function generateCSRFToken(): ?string {
// Use Moat to generate token, if we have it.
$event = Civi\Core\Event\GenericHookEvent::create(['createdToken' => '']);
Civi::dispatcher()->dispatch('civi.moat.token', $event);
if ($event->createdToken) {
return $event->createdToken;
}
// Use Firewall if we have it.
if (class_exists('\\Civi\Firewall\Firewall')) {
return (new \Civi\Firewall\Firewall())->generateCSRFToken();
}
return null;
}
/**
* Confirm that transaction id is legit and went through.
*
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment