diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index d51f6bd0ab..19a3a847a7 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -1,5 +1,7 @@ # WIP Release notes for Commerce 5.6 +- Cart controller actions that accept an explicit cart number are now rate limited to mitigate enumeration attacks. +- Cart numbers are now generated using a cryptographically secure random number generator. - Fixed a bug where Variant with empty SKUs didn't show a validation error when saving a product after it was duplicated. ([#4197](https://github.com/craftcms/commerce/issues/4197)) - Shipping rule categories are now eager loaded on shipping rules automatically. ([#4220](https://github.com/craftcms/commerce/issues/4220)) - Added `craft\commerce\services\ShippingRuleCategories::getShippingRuleCategoriesByRuleIds()`. diff --git a/src/controllers/CartController.php b/src/controllers/CartController.php index 43aaf18298..61c3640a21 100644 --- a/src/controllers/CartController.php +++ b/src/controllers/CartController.php @@ -19,10 +19,13 @@ use craft\elements\User; use craft\errors\ElementNotFoundException; use craft\errors\MissingComponentException; -use craft\helpers\Json; use craft\helpers\StringHelper; use craft\helpers\UrlHelper; use Illuminate\Support\Collection; +use thamtech\ratelimiter\Context; +use thamtech\ratelimiter\handlers\TooManyRequestsHttpExceptionHandler; +use thamtech\ratelimiter\limit\RateLimit; +use thamtech\ratelimiter\RateLimiter; use Throwable; use yii\base\Exception; use yii\base\InvalidConfigException; @@ -76,6 +79,43 @@ public function init(): void parent::init(); } + /** + * @inheritdoc + */ + public function behaviors(): array + { + return parent::behaviors() + [ + 'rateLimiter' => [ + 'class' => RateLimiter::class, + 'only' => ['get-cart', 'update-cart', 'load-cart', 'complete'], + 'components' => [ + 'rateLimit' => [ + 'definitions' => [ + 'cart-by-number' => [ + 'class' => RateLimit::class, + 'limit' => 1, + 'window' => 1, + // Only apply rate limiting when a cart number is explicitly passed + 'active' => function(Context $context, $rateLimitId) { + return $context->request->getBodyParam('number') || $context->request->getQueryParam('number'); + }, + 'identifier' => fn(Context $context, $rateLimitId) => sprintf( + '%s:%s', + $rateLimitId, + $context->request->getUserIP(), + ), + ], + ], + ], + 'allowanceStorage' => [ + 'cache' => 'cache', + ], + ], + 'as tooManyRequestsException' => TooManyRequestsHttpExceptionHandler::class, + ], + ]; + } + /** * Returns the cart as JSON * diff --git a/src/services/Carts.php b/src/services/Carts.php index cde1ccd525..76a3a576be 100644 --- a/src/services/Carts.php +++ b/src/services/Carts.php @@ -279,7 +279,7 @@ public function forgetCart(): void */ public function generateCartNumber(): string { - return md5(uniqid((string)mt_rand(), true)); + return bin2hex(random_bytes(16)); } /**