From 32584dae5b441b1a0f963d476d61fb246e2fbc78 Mon Sep 17 00:00:00 2001 From: Guillaume AGNIERAY Date: Mon, 5 Oct 2026 16:30:32 +0200 Subject: [PATCH] Throttle public form submissions Co-authored-by: Johan Cwiklinski --- .../Controllers/HelloassoController.php | 40 ++++++++++++++++++- lib/GaletteHelloasso/Helloasso.php | 12 ++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/lib/GaletteHelloasso/Controllers/HelloassoController.php b/lib/GaletteHelloasso/Controllers/HelloassoController.php index 89d9e93..fe84fa8 100644 --- a/lib/GaletteHelloasso/Controllers/HelloassoController.php +++ b/lib/GaletteHelloasso/Controllers/HelloassoController.php @@ -13,7 +13,9 @@ use Analog\Analog; use DI\Attribute\Inject; use Galette\Controllers\AbstractPluginController; +use Galette\Core\AuthThrottle; use Galette\Core\History; +use Galette\Core\Login; use Galette\Entity\Adherent; use Galette\Entity\Contribution; use Galette\Entity\ContributionsTypes; @@ -45,14 +47,22 @@ class HelloassoController extends AbstractPluginController #[Inject] protected Helloasso $helloasso; + private const string THROTTLE_SCOPE = 'helloasso-payment'; + /** * Main form */ - public function form(Response $response): Response + public function form(Response $response, AuthThrottle $throttle, Login $login): Response { $helloasso = $this->helloasso; $current_url = $this->preferences->getURL(); + $address = $helloasso->getUserIPAddress(); + + // When a member is logged there is nothing left to hold against its address + if ($login->isLogged() && $address !== '') { + $throttle->clearEvent(self::THROTTLE_SCOPE, $address); + } $params = [ 'helloasso' => $helloasso, @@ -90,12 +100,38 @@ public function form(Response $response): Response /** * Checkout form */ - public function formCheckout(Request $request, Response $response): Response + public function formCheckout(Request $request, Response $response, AuthThrottle $throttle, Login $login): Response { $helloasso_request = $request->getParsedBody(); $helloasso = $this->helloasso; $adherent = new Adherent($this->zdb); + $address = $helloasso->getUserIPAddress(); + + // Throttle public form submissions + if (!$login->isLogged() && $address !== '') { + // Asked before anything is done, so that a caller being refused costs + // nothing but a lookup + $delay = $throttle->getDelayForEvent(self::THROTTLE_SCOPE, $address); + if ($delay > 0) { + $this->flash->addMessage( + 'error_detected', + str_replace( + '%seconds', + (string)$delay, + _T("Too many requests. Please try again in %seconds seconds.", "helloasso") + ) + ); + return $response + ->withStatus(301) + ->withHeader('Location', $this->routeparser->urlFor('helloasso_form')); + } + + // The attempt is counted, not its outcome: what is limited is how many + // times the form can be submitted, whatever it answers + $throttle->recordEvent(self::THROTTLE_SCOPE, $address, 120, 3600); + } + // Only reasons proposed to the current user can be paid $item_id = (int)($helloasso_request['item_id'] ?? 0); $helloasso_amounts = $helloasso->getAmounts($this->login); diff --git a/lib/GaletteHelloasso/Helloasso.php b/lib/GaletteHelloasso/Helloasso.php index 0a6d904..98950ba 100644 --- a/lib/GaletteHelloasso/Helloasso.php +++ b/lib/GaletteHelloasso/Helloasso.php @@ -11,7 +11,9 @@ namespace GaletteHelloasso; use Analog\Analog; +use Galette\Core\AuthThrottle; use Galette\Core\Db; +use Galette\Core\History; use Galette\Core\Login; use Galette\Core\Preferences; use Galette\Entity\ContributionsTypes; @@ -733,4 +735,14 @@ public function unsetInactives(): void { $this->inactives = []; } + + /** + * Get the user IP address + */ + public function getUserIPAddress(): string + { + // History resolves the address the way the rest of Galette does, honouring + // GALETTE_X_FORWARDED_FOR_INDEX when the site sits behind a proxy + return AuthThrottle::normalizeAddress(History::findUserIPAddress()); + } }