diff --git a/app/code/Magento/Contact/Model/Mail.php b/app/code/Magento/Contact/Model/Mail.php index 43c1974252b5a..1baa1b7f476e5 100644 --- a/app/code/Magento/Contact/Model/Mail.php +++ b/app/code/Magento/Contact/Model/Mail.php @@ -1,7 +1,7 @@ contactsConfig = $contactsConfig; $this->transportBuilder = $transportBuilder; $this->inlineTranslation = $inlineTranslation; $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); + $this->validatorFactory = $validatorFactory; } /** @@ -59,9 +70,13 @@ public function __construct( * @param string $replyTo * @param array $variables * @return void + * @throws ValidatorException */ public function send($replyTo, array $variables) { + // Perform validation before sending email + $this->_validate($variables['data']); + /** @see \Magento\Contact\Controller\Index\Post::validatedParams() */ $replyToName = !empty($variables['data']['name']) ? $variables['data']['name'] : null; @@ -86,4 +101,23 @@ public function send($replyTo, array $variables) $this->inlineTranslation->resume(); } } + + /** + * Validate the contact form data. + * + * @param DataObject $data + * @throws ValidatorException + */ + protected function _validate(DataObject $data) + { + $validator = $this->validatorFactory->createValidator('contact', 'save'); + + if (!$validator->isValid($data)) { + throw new ValidatorException( + null, + null, + $validator->getMessages() + ); + } + } } diff --git a/app/code/Magento/Contact/Model/Validator/Email.php b/app/code/Magento/Contact/Model/Validator/Email.php new file mode 100644 index 0000000000000..baf98d62254b0 --- /dev/null +++ b/app/code/Magento/Contact/Model/Validator/Email.php @@ -0,0 +1,98 @@ +emailValidator = $emailValidator; + } + + /** + * Validate email fields. + * + * @param DataObject $data + * @return bool + */ + public function isValid($data): bool + { + if (!$this->emailValidator->isValidationEnabled()) { + return true; + } + + $email = $data->getData('email'); + if (empty($email)) { + return true; + } + + if (!$this->validateEmailField($email)) { + return false; + } + + return count($this->_messages) == 0; + } + + /** + * Validate the email field. + * + * @param string|null $emailValue + * @return bool + */ + protected function validateEmailField(?string $emailValue): bool + { + if (!$this->emailValidator->isValid($emailValue)) { + parent::_addMessages( + [ + __( + 'Email address is not valid! Allowed characters: %1', + $this->emailValidator->allowedCharsDescription + ), + ] + ); + return false; + } + + if ($this->isBlacklistEmail($emailValue)) { + parent::_addMessages([ + __('The email address or domain is blacklisted.') + ]); + return false; + } + + return true; + } + + /** + * Check if email field is blacklisted using the EmailAddressValidator. + * + * @param string|null $emailValue + * @return bool + */ + protected function isBlacklistEmail(?string $emailValue): bool + { + return $this->emailValidator->isBlacklist($emailValue); + } +} diff --git a/app/code/Magento/Contact/Model/Validator/ForbiddenPattern.php b/app/code/Magento/Contact/Model/Validator/ForbiddenPattern.php new file mode 100644 index 0000000000000..2bb404c846d16 --- /dev/null +++ b/app/code/Magento/Contact/Model/Validator/ForbiddenPattern.php @@ -0,0 +1,62 @@ +forbiddenValidator = $forbiddenValidator; + } + + /** + * Validate contact form data fields against forbidden patterns. + * + * @param DataObject $data + * @return bool + * @throws LocalizedException + */ + public function isValid($data): bool + { + if (!$this->forbiddenValidator->isValidationEnabled()) { + return true; + } + + $dataFields = $data->getData(); + if (empty($dataFields)) { + return true; + } + + $isValid = $this->forbiddenValidator->validateDataRecursively($dataFields); + + if (!$isValid) { + parent::_addMessages([ + __('Fraud Protection: Forbidden pattern detected in contact form data') + ]); + } + + return count($this->_messages) == 0; + } +} diff --git a/app/code/Magento/Contact/etc/validation.xml b/app/code/Magento/Contact/etc/validation.xml new file mode 100644 index 0000000000000..6bf33cc0e6217 --- /dev/null +++ b/app/code/Magento/Contact/etc/validation.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Customer/Model/Validator/City.php b/app/code/Magento/Customer/Model/Validator/City.php index 0b53551dfd88f..b2e8aedd8ca3e 100644 --- a/app/code/Magento/Customer/Model/Validator/City.php +++ b/app/code/Magento/Customer/Model/Validator/City.php @@ -1,7 +1,7 @@ cityValidator = $cityValidator; + } /** - * Validate city fields. + * Validate city field. * - * @param Customer $customer + * @param Customer $entity * @return bool */ - public function isValid($customer) + public function isValid($entity): bool { - if (!$this->isValidCity($customer->getCity())) { - parent::_addMessages([[ - 'city' => "Invalid City. Please use A-Z, a-z, 0-9, -, ', spaces" - ]]); + if (!$this->cityValidator->isValidationEnabled()) { + return true; + } + + $cityField = $entity->getCity(); + if (empty($cityField)) { + return true; + } + + if (!$this->validateCityField($cityField)) { + parent::_addMessages( + [ + __( + '%1 is not valid! Allowed characters: %2', + 'City', + $this->cityValidator->allowedCharsDescription + ), + ] + ); } return count($this->_messages) == 0; } /** - * Check if city field is valid. + * Validate the city field. * * @param string|null $cityValue * @return bool */ - private function isValidCity($cityValue) + private function validateCityField(?string $cityValue): bool { - if ($cityValue != null) { - if (preg_match(self::PATTERN_CITY, $cityValue, $matches)) { - return $matches[0] == $cityValue; - } - } - - return true; + return $this->cityValidator->isValid($cityValue); } } diff --git a/app/code/Magento/Customer/Model/Validator/Email.php b/app/code/Magento/Customer/Model/Validator/Email.php new file mode 100644 index 0000000000000..be1d1905104fd --- /dev/null +++ b/app/code/Magento/Customer/Model/Validator/Email.php @@ -0,0 +1,98 @@ +emailValidator = $emailValidator; + } + + /** + * Validate email fields. + * + * @param Customer $customer + * @return bool + */ + public function isValid($customer): bool + { + if (!$this->emailValidator->isValidationEnabled()) { + return true; + } + + $email = $customer->getEmail(); + if (empty($email)) { + return true; + } + + if (!$this->validateEmailField($email)) { + return false; + } + + return count($this->_messages) == 0; + } + + /** + * Validate the email field. + * + * @param string|null $emailValue + * @return bool + */ + private function validateEmailField(?string $emailValue): bool + { + if (!$this->emailValidator->isValid($emailValue)) { + parent::_addMessages( + [ + __( + 'Email address is not valid! Allowed characters: %1', + $this->emailValidator->allowedCharsDescription + ), + ] + ); + return false; + } + + if ($this->isBlacklistEmail($emailValue)) { + parent::_addMessages([ + __('The email address or domain is blacklisted.') + ]); + return false; + } + + return true; + } + + /** + * Check if email field is blacklisted using the EmailAddressValidator. + * + * @param string|null $emailValue + * @return bool + */ + private function isBlacklistEmail(?string $emailValue): bool + { + return $this->emailValidator->isBlacklist($emailValue); + } +} diff --git a/app/code/Magento/Customer/Model/Validator/ForbiddenPattern.php b/app/code/Magento/Customer/Model/Validator/ForbiddenPattern.php new file mode 100644 index 0000000000000..be7377e0e8e4a --- /dev/null +++ b/app/code/Magento/Customer/Model/Validator/ForbiddenPattern.php @@ -0,0 +1,62 @@ +forbiddenValidator = $forbiddenValidator; + } + + /** + * Validate EAV data fields against forbidden patterns. + * + * @param mixed $customer + * @return bool + * @throws LocalizedException + */ + public function isValid($customer): bool + { + if (!$this->forbiddenValidator->isValidationEnabled()) { + return true; + } + + $customerData = $customer->getData(); + if (empty($customerData)) { + return true; + } + + $isValid = $this->forbiddenValidator->validateDataRecursively($customerData); + + if (!$isValid) { + parent::_addMessages([ + __('Fraud Protection: Forbidden pattern detected in customer data') + ]); + } + + return count($this->_messages) == 0; + } +} diff --git a/app/code/Magento/Customer/Model/Validator/Name.php b/app/code/Magento/Customer/Model/Validator/Name.php index 75d460358970c..ff714030cadb1 100644 --- a/app/code/Magento/Customer/Model/Validator/Name.php +++ b/app/code/Magento/Customer/Model/Validator/Name.php @@ -1,7 +1,7 @@ nameValidator = $nameValidator; + } /** * Validate name fields. @@ -23,8 +37,8 @@ class Name extends AbstractValidator * @param Customer $customer * @return bool */ - public function isValid($customer) - { + public function isValid($customer): bool + { if (!$this->isValidName($customer->getFirstname())) { parent::_addMessages([['firstname' => 'First Name is not valid!']]); } @@ -36,24 +50,18 @@ public function isValid($customer) if (!$this->isValidName($customer->getMiddlename())) { parent::_addMessages([['middlename' => 'Middle Name is not valid!']]); } - + return count($this->_messages) == 0; } /** - * Check if name field is valid. + * Check if name field is valid using the NameValidator. * * @param string|null $nameValue * @return bool */ - private function isValidName($nameValue) + private function isValidName($nameValue): bool { - if ($nameValue != null) { - if (preg_match(self::PATTERN_NAME, $nameValue, $matches)) { - return $matches[0] == $nameValue; - } - } - - return true; + return $this->nameValidator->isValid($nameValue); } } diff --git a/app/code/Magento/Customer/Model/Validator/Pattern/CityValidator.php b/app/code/Magento/Customer/Model/Validator/Pattern/CityValidator.php new file mode 100644 index 0000000000000..c188ec049533e --- /dev/null +++ b/app/code/Magento/Customer/Model/Validator/Pattern/CityValidator.php @@ -0,0 +1,111 @@ +scopeConfig = $scopeConfig; + } + + /** + * Check if both the global security pattern and city validation are enabled in the configuration. + * + * @return bool + */ + public function isValidationEnabled(): bool + { + $isGlobalPatternEnabled = $this->scopeConfig->isSetFlag( + self::XML_PATH_SECURITY_PATTERN_ENABLED, + ScopeInterface::SCOPE_STORE + ); + + $isCityValidationEnabled = $this->scopeConfig->isSetFlag( + self::XML_PATH_SECURITY_PATTERN_CITY_ENABLED, + ScopeInterface::SCOPE_STORE + ); + + return $isGlobalPatternEnabled && $isCityValidationEnabled; + } + + /** + * Validate the city value against the pattern. + * + * @param mixed $value + * @return bool + */ + public function isValid($value): bool + { + if ($value === null || $value === '' || !is_string($value)) { + return true; + } + + return preg_match($this->patternCity, trim($value)) === 1; + + return false; + } +} diff --git a/app/code/Magento/Customer/Model/Validator/Pattern/EmailAddressValidator.php b/app/code/Magento/Customer/Model/Validator/Pattern/EmailAddressValidator.php new file mode 100644 index 0000000000000..0a6d75505b2aa --- /dev/null +++ b/app/code/Magento/Customer/Model/Validator/Pattern/EmailAddressValidator.php @@ -0,0 +1,143 @@ +scopeConfig = $scopeConfig; + $this->emailValidator = $emailValidator; + } + + /** + * Check if both the global security pattern and email validation are enabled in the configuration. + * + * @return bool + */ + public function isValidationEnabled(): bool + { + $isGlobalPatternEnabled = $this->scopeConfig->isSetFlag( + self::XML_PATH_SECURITY_PATTERN_ENABLED, + ScopeInterface::SCOPE_STORE + ); + + $isEmailValidationEnabled = $this->scopeConfig->isSetFlag( + self::XML_PATH_SECURITY_PATTERN_EMAIL_ENABLED, + ScopeInterface::SCOPE_STORE + ); + + return $isGlobalPatternEnabled && $isEmailValidationEnabled; + } + + /** + * Validate an email address. + * + * @param mixed $value + * @return bool + */ + public function isValid($value): bool + { + if ($value === null || $value === '' || !is_string($value)) { + return false; + } + + if (!preg_match($this->patterEmail, trim($value))) { + return false; + } + + return $this->emailValidator->isValid($value); + } + + /** + * Check if the email address or its domain is blacklisted. + * + * @param string|null $emailValue + * @return bool + */ + public function isBlacklist(?string $emailValue): bool + { + if ($emailValue === null || $emailValue === '' || !is_string($emailValue)) { + return false; + } + + if ($this->blacklistArray === null) { + $blacklist = $this->scopeConfig->getValue(self::XML_PATH_SECURITY_PATTERN_MAIL_BLACKLIST); + $this->blacklistArray = !empty($blacklist) ? preg_split('/[\r\n,]+/', $blacklist) : []; + } + + $emailHost = substr(strrchr($emailValue, "@"), 1); + + return in_array($emailValue, $this->blacklistArray) || in_array($emailHost, $this->blacklistArray); + } +} diff --git a/app/code/Magento/Customer/Model/Validator/Pattern/ForbiddenValidator.php b/app/code/Magento/Customer/Model/Validator/Pattern/ForbiddenValidator.php new file mode 100644 index 0000000000000..e552676de8f56 --- /dev/null +++ b/app/code/Magento/Customer/Model/Validator/Pattern/ForbiddenValidator.php @@ -0,0 +1,135 @@ +scopeConfig = $scopeConfig; + } + + /** + * Check if forbidden patterns validation is enabled. + * + * @return bool + */ + public function isValidationEnabled(): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_SECURITY_CODE_INJECTION_ENABLED, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Returns an array of forbidden patterns. + * + * @return string[] + */ + public function getPatterns(): array + { + return [ + '/{{.*}}/', + '/<\?=/', + '/<\?php/', + '/shell_exec/', + '/eval\(/', + '/\${IFS%/', + '/\bcurl\b/', + ]; + } + + /** + * Validates the given field value against forbidden patterns. + * + * @param mixed $value + * @return bool + */ + public function isValid($value): bool + { + if (!$this->isValidationEnabled()) { + return true; + } + + return $this->validatePattern($value); + } + + /** + * Recursively validate data against forbidden patterns. + * + * @param mixed $data + * @return bool + */ + public function validateDataRecursively($data): bool + { + if (is_array($data)) { + foreach ($data as $value) { + if (!$this->validateDataRecursively($value)) { + return false; + } + } + } else { + return $this->isValid($data); + } + + return true; + } + + /** + * Validates the field value against forbidden patterns. + * + * @param mixed $value + * @return bool + */ + private function validatePattern(mixed $value): bool + { + if (!is_string($value) || trim($value) === '') { + return true; + } + + foreach ($this->getPatterns() as $pattern) { + if (preg_match($pattern, trim($value))) { + return false; + } + } + + if (preg_match('/base64_decode\(/', $value)) { + // Use of base64_decode is discouraged, ensure this is safe in your context + // @codingStandardsIgnoreLine + $decodedValue = base64_decode($value); + return $this->validatePattern($decodedValue); + } + + return true; + } +} diff --git a/app/code/Magento/Customer/Model/Validator/Pattern/NameValidator.php b/app/code/Magento/Customer/Model/Validator/Pattern/NameValidator.php new file mode 100644 index 0000000000000..e0986e9e04d7b --- /dev/null +++ b/app/code/Magento/Customer/Model/Validator/Pattern/NameValidator.php @@ -0,0 +1,127 @@ +scopeConfig = $scopeConfig; + } + + /** + * Check if both the global security pattern and name validation are enabled in the configuration. + * + * @return bool + */ + public function isValidationEnabled(): bool + { + $isGlobalPatternEnabled = $this->scopeConfig->isSetFlag( + self::XML_PATH_SECURITY_PATTERN_ENABLED, + ScopeInterface::SCOPE_STORE + ); + + $isNameValidationEnabled = $this->scopeConfig->isSetFlag( + self::XML_PATH_SECURITY_PATTERN_NAME_ENABLED, + ScopeInterface::SCOPE_STORE + ); + + return $isGlobalPatternEnabled && $isNameValidationEnabled; + } + + /** + * Validate the name value against the pattern. + * + * @param mixed $value + * @return bool + */ + public function isValid($value): bool + { + if ($value === null) { + return true; + } + + $pattern = $this->isValidationEnabled() ? $this->patternName : self::PATTERN_NAME; + + if (is_string($value)) { + $trimmedValue = trim($value); + if (preg_match($pattern, $trimmedValue, $matches)) { + return $matches[0] === $trimmedValue; + } + } + + return true; + } +} diff --git a/app/code/Magento/Customer/Model/Validator/Pattern/StreetValidator.php b/app/code/Magento/Customer/Model/Validator/Pattern/StreetValidator.php new file mode 100644 index 0000000000000..37b576354655d --- /dev/null +++ b/app/code/Magento/Customer/Model/Validator/Pattern/StreetValidator.php @@ -0,0 +1,147 @@ +scopeConfig = $scopeConfig; + } + + /** + * Check if both the global security pattern and street validation are enabled in the configuration. + * + * @return bool + */ + public function isValidationEnabled(): bool + { + $isGlobalPatternEnabled = $this->scopeConfig->isSetFlag( + self::XML_PATH_SECURITY_PATTERN_ENABLED, + ScopeInterface::SCOPE_STORE + ); + + $isStreetValidationEnabled = $this->scopeConfig->isSetFlag( + self::XML_PATH_SECURITY_PATTERN_STREET_ENABLED, + ScopeInterface::SCOPE_STORE + ); + + return $isGlobalPatternEnabled && $isStreetValidationEnabled; + } + + /** + * Validate a street address string or an array of street address strings. + * + * @param mixed $value + * @return bool + */ + public function isValid($value): bool + { + if (!$this->isValidationEnabled()) { + return true; // Skip validation if globally disabled + } + + if (is_array($value)) { + foreach ($value as $streetValue) { + if (!$this->validateSingleStreet($streetValue)) { + return false; + } + } + } else { + if (!$this->validateSingleStreet($value)) { + return false; + } + } + + return true; + } + + /** + * Validate a single street address string. + * + * @param mixed $streetValue + * @return bool + */ + private function validateSingleStreet($streetValue): bool + { + return $this->isValidStreet($streetValue); + } + + /** + * Check if the street field is valid. + * + * @param mixed $streetValue + * @return bool + */ + private function isValidStreet(mixed $streetValue): bool + { + if ($streetValue === null || $streetValue === '' || !is_string($streetValue)) { + return true; + } + + return preg_match($this->patterStreet, trim($streetValue)) === 1; + } +} diff --git a/app/code/Magento/Customer/Model/Validator/Pattern/TelephoneValidator.php b/app/code/Magento/Customer/Model/Validator/Pattern/TelephoneValidator.php new file mode 100644 index 0000000000000..7bbd04085e720 --- /dev/null +++ b/app/code/Magento/Customer/Model/Validator/Pattern/TelephoneValidator.php @@ -0,0 +1,109 @@ +scopeConfig = $scopeConfig; + } + + /** + * Check if both the global security pattern and telephone validation are enabled in the configuration. + * + * @return bool + */ + public function isValidationEnabled(): bool + { + // Check if the global security pattern validation is enabled + $isGlobalPatternEnabled = $this->scopeConfig->isSetFlag( + self::XML_PATH_SECURITY_PATTERN_ENABLED, + ScopeInterface::SCOPE_STORE + ); + + // Check if the specific telephone validation is enabled + $isTelephoneValidationEnabled = $this->scopeConfig->isSetFlag( + self::XML_PATH_SECURITY_PATTERN_TELEPHONE_ENABLED, + ScopeInterface::SCOPE_STORE + ); + + // Return true only if both are enabled + return $isGlobalPatternEnabled && $isTelephoneValidationEnabled; + } + + /** + * Validate the telephone value against the pattern. + * + * @param mixed $value + * @return bool + */ + public function isValid($value): bool + { + if ($value === null || $value === '') { + return true; + } + + return preg_match($this->patternTelephone, trim($value)) === 1; + } +} diff --git a/app/code/Magento/Customer/Model/Validator/Street.php b/app/code/Magento/Customer/Model/Validator/Street.php index 7de57d0ed32ef..5311eb526c2c6 100644 --- a/app/code/Magento/Customer/Model/Validator/Street.php +++ b/app/code/Magento/Customer/Model/Validator/Street.php @@ -1,7 +1,7 @@ streetValidator = $streetValidator; + } /** - * Validate street fields. + * Validate street field. * * @param Customer $customer * @return bool */ - public function isValid($customer) + public function isValid($customer): bool { - foreach ($customer->getStreet() as $street) { - if (!$this->isValidStreet($street)) { - parent::_addMessages([[ - 'street' => "Invalid Street Address. Please use A-Z, a-z, 0-9, , - . ' ’ ` & spaces" - ]]); + if (!$this->streetValidator->isValidationEnabled()) { + return true; + } + + $streets = $customer->getStreet(); + if (empty($streets)) { + return true; + } + + foreach ($streets as $street) { + if (!$this->validateStreetField($street)) { + parent::_addMessages( + [ + 'street' => __( + 'Street is not valid! Allowed characters: %1', + $this->streetValidator->allowedCharsDescription + ), + ] + ); } } @@ -50,19 +65,13 @@ public function isValid($customer) } /** - * Check if street field is valid. + * Validate the street field. * * @param string|null $streetValue * @return bool */ - private function isValidStreet($streetValue) + private function validateStreetField(?string $streetValue): bool { - if ($streetValue != null) { - if (preg_match(self::PATTERN_STREET, $streetValue, $matches)) { - return $matches[0] == $streetValue; - } - } - - return true; + return $this->streetValidator->isValid($streetValue); } } diff --git a/app/code/Magento/Customer/Model/Validator/Telephone.php b/app/code/Magento/Customer/Model/Validator/Telephone.php index 0c85cb51f7e3d..78ba26854669d 100644 --- a/app/code/Magento/Customer/Model/Validator/Telephone.php +++ b/app/code/Magento/Customer/Model/Validator/Telephone.php @@ -1,7 +1,7 @@ telephoneValidator = $telephoneValidator; + } + /** * Validate telephone fields. * * @param Customer $customer * @return bool */ - public function isValid($customer) + public function isValid($customer): bool { - if (!$this->isValidTelephone($customer->getTelephone())) { - parent::_addMessages([[ - 'telephone' => "Invalid Phone Number. Please use 0-9, +, -, (, ) and space." - ]]); + if (!$this->telephoneValidator->isValidationEnabled()) { + return true; + } + + $telephoneFields = [ + 'Phone Number' => $customer->getTelephone(), + 'Fax Number' => $customer->getFax() + ]; + + foreach ($telephoneFields as $fieldName => $fieldValue) { + if (!empty($fieldValue) && !$this->validateTelephoneField($fieldValue)) { + parent::_addMessages( + [ + __( + '%1 is not valid! Allowed characters: %2', + $fieldName, + $this->telephoneValidator->allowedCharsDescription + ), + ] + ); + } } return count($this->_messages) == 0; } /** - * Check if telephone field is valid. + * Validate a single telephone field. * - * @param string|null $telephoneValue + * @param int|string|null $telephoneValue * @return bool */ - private function isValidTelephone($telephoneValue) + private function validateTelephoneField(int|string|null $telephoneValue): bool { - if ($telephoneValue != null) { - if (preg_match(self::PATTERN_TELEPHONE, (string) $telephoneValue, $matches)) { - return $matches[0] == $telephoneValue; - } - } - - return true; + return $this->telephoneValidator->isValid($telephoneValue); } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Validator/CityTest.php b/app/code/Magento/Customer/Test/Unit/Model/Validator/CityTest.php index af46e7f5c7748..87626308e324d 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Validator/CityTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Validator/CityTest.php @@ -1,39 +1,47 @@ nameValidator = new City; - $this->customerMock = $this + $this->cityValidatorMock = $this->createMock(CityValidator::class); + $this->cityValidator = new City($this->cityValidatorMock); + + $this->addressMock = $this ->getMockBuilder(Customer::class) ->disableOriginalConstructor() ->addMethods(['getCity']) @@ -41,45 +49,71 @@ protected function setUp(): void } /** - * Test for allowed apostrophe and other punctuation characters in customer names + * Test for valid city name * * @param string $city - * @param string $message + * @param bool $expectedIsValid * @return void - * @dataProvider expectedPunctuationInNamesDataProvider + * @dataProvider expectedPunctuationInCityDataProvider */ - public function testValidateCorrectPunctuationInNames( + public function testValidateCityName( string $city, - string $message - ) { - $this->customerMock->expects($this->once())->method('getCity')->willReturn($city); + bool $expectedIsValid + ): void { + $this->addressMock->expects($this->once())->method('getCity')->willReturn($city); - $isValid = $this->nameValidator->isValid($this->customerMock); - $this->assertTrue($isValid, $message); + $isValid = $this->cityValidator->isValid($this->addressMock); + $this->assertEquals($expectedIsValid, $isValid); } /** + * Data provider for city names + * * @return array */ public static function expectedPunctuationInNamesDataProvider(): array { return [ + [ + 'city' => 'New York', + 'expectedIsValid' => true, + 'message' => 'Spaces must be allowed in city names' + ], + [ + 'city' => 'São Paulo', + 'expectedIsValid' => true, + 'message' => 'Accented characters and spaces must be allowed in city names' + ], + [ + 'city' => 'St. Louis', + 'expectedIsValid' => true, + 'message' => 'Periods and spaces must be allowed in city names' + ], [ 'city' => 'Москва', - 'message' => 'Unicode letters must be allowed in city' + 'expectedIsValid' => true, + 'message' => 'Unicode letters must be allowed in city names' + ], + [ + 'city' => 'Moscow \'', + 'expectedIsValid' => true, + 'message' => 'Apostrophe characters must be allowed in city names' ], [ - 'city' => 'Мо́сква', - 'message' => 'Unicode marks must be allowed in city' + 'city' => 'St.-Pierre', + 'expectedIsValid' => true, + 'message' => 'Hyphens must be allowed in city names' ], [ - 'city' => ' Moscow \'', - 'message' => 'Apostrophe characters must be allowed in city' + 'city' => 'Offenbach (Main)', + 'expectedIsValid' => true, + 'message' => 'Parentheses must be allowed in city names' ], [ - 'city' => ' Moscow Moscow', - 'message' => 'Whitespace characters must be allowed in city' - ] + 'city' => 'Rome: The Eternal City', + 'expectedIsValid' => true, + 'message' => 'Colons must be allowed in city names' + ], ]; } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Validator/NameTest.php b/app/code/Magento/Customer/Test/Unit/Model/Validator/NameTest.php index c141b3baa423f..a9f1be62b3704 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Validator/NameTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Validator/NameTest.php @@ -1,7 +1,7 @@ nameValidator = new Name; + $this->nameValidatorMock = $this->createMock(NameValidator::class); + $this->nameValidator = new Name($this->nameValidatorMock); $this->customerMock = $this ->getMockBuilder(Customer::class) ->disableOriginalConstructor() @@ -41,7 +48,7 @@ protected function setUp(): void } /** - * Test for allowed apostrophe and other punctuation characters in customer names + * Test for allowed punctuation characters in customer names * * @param string $firstName * @param string $middleName diff --git a/app/code/Magento/Customer/Test/Unit/Model/Validator/StreetTest.php b/app/code/Magento/Customer/Test/Unit/Model/Validator/StreetTest.php index e8d4f7b61be7a..85868689dd87a 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Validator/StreetTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Validator/StreetTest.php @@ -1,39 +1,47 @@ nameValidator = new Street; - $this->customerMock = $this + $this->streetValidatorMock = $this->createMock(StreetValidator::class); + $this->streetValidator = new Street($this->streetValidatorMock); + + $this->addressMock = $this ->getMockBuilder(Customer::class) ->disableOriginalConstructor() ->addMethods(['getStreet']) @@ -41,24 +49,26 @@ protected function setUp(): void } /** - * Test for allowed apostrophe and other punctuation characters in customer names + * Test for valid street name * * @param array $street * @param string $message * @return void - * @dataProvider expectedPunctuationInNamesDataProvider + * @dataProvider expectedPunctuationInStreetDataProvider */ - public function testValidateCorrectPunctuationInNames( + public function testValidateStreetName( array $street, string $message - ) { - $this->customerMock->expects($this->once())->method('getStreet')->willReturn($street); + ): void { + $this->addressMock->expects($this->once())->method('getStreet')->willReturn($street); - $isValid = $this->nameValidator->isValid($this->customerMock); + $isValid = $this->streetValidator->isValid($this->addressMock); $this->assertTrue($isValid, $message); } /** + * Data provider for street names + * * @return array */ public static function expectedPunctuationInNamesDataProvider(): array @@ -102,7 +112,7 @@ public static function expectedPunctuationInNamesDataProvider(): array 'O`Connell Street', '321 Birch Boulevard ’Willow Retreat’' ], - 'message' => 'quotes must be allowed in street' + 'message' => 'Quotes must be allowed in street' ], [ 'street' => [ @@ -127,6 +137,14 @@ public static function expectedPunctuationInNamesDataProvider(): array '876 Elm Way' ], 'message' => 'Digits must be allowed in street' + ], + [ + 'street' => [ + '1234 Elm St. [Apartment 5]', + 'Main St. (Suite 200)', + '456 Pine St. [Unit 10]' + ], + 'message' => 'Square brackets and parentheses must be allowed in street' ] ]; } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Validator/TelephoneTest.php b/app/code/Magento/Customer/Test/Unit/Model/Validator/TelephoneTest.php index 859c240764e57..374622fecd8d3 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Validator/TelephoneTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Validator/TelephoneTest.php @@ -1,7 +1,7 @@ nameValidator = new Telephone; + $this->telephoneValidatorMock = $this->createMock(TelephoneValidator::class); + $this->telephoneValidator = new Telephone($this->telephoneValidatorMock); $this->customerMock = $this ->getMockBuilder(Customer::class) ->disableOriginalConstructor() - ->addMethods(['getTelephone']) + ->addMethods(['getTelephone', 'getFax']) ->getMock(); } /** - * Test for allowed apostrophe and other punctuation characters in customer names + * Test for allowed punctuation characters in customer telephone numbers * * @param string $telephone + * @param string $fax * @param string $message * @return void - * @dataProvider expectedPunctuationInNamesDataProvider + * @dataProvider expectedPunctuationInTelephoneDataProvider */ - public function testValidateCorrectPunctuationInNames( + public function testValidateCorrectPunctuationInTelephone( string $telephone, + string $fax, string $message - ) { + ): void { $this->customerMock->expects($this->once())->method('getTelephone')->willReturn($telephone); + $this->customerMock->expects($this->once())->method('getFax')->willReturn($fax); - $isValid = $this->nameValidator->isValid($this->customerMock); + $isValid = $this->telephoneValidator->isValid($this->customerMock); $this->assertTrue($isValid, $message); } @@ -66,19 +76,18 @@ public static function expectedPunctuationInNamesDataProvider(): array return [ [ 'telephone' => '(1)99887766', - 'message' => 'parentheses must be allowed in telephone' + 'fax' => '123456789', + 'message' => 'Parentheses must be allowed in telephone, and digits must be allowed in fax.' ], [ 'telephone' => '+6255554444', - 'message' => 'plus sign be allowed in telephone' + 'fax' => '123 456 789', + 'message' => 'Plus sign must be allowed in telephone, and spaces must be allowed in fax.' ], [ 'telephone' => '555-555-555', - 'message' => 'hyphen must be allowed in telephone' - ], - [ - 'telephone' => '123456789', - 'message' => 'Digits (numbers) must be allowed in telephone' + 'fax' => '123/456/789', + 'message' => 'Hyphen must be allowed in telephone, and forward slashes must be allowed in fax.' ] ]; } diff --git a/app/code/Magento/Customer/etc/adminhtml/system.xml b/app/code/Magento/Customer/etc/adminhtml/system.xml index ec76e09fdf459..de12213ba5f4e 100644 --- a/app/code/Magento/Customer/etc/adminhtml/system.xml +++ b/app/code/Magento/Customer/etc/adminhtml/system.xml @@ -1,8 +1,8 @@ @@ -313,5 +313,67 @@ +
+ + + + + Magento\Config\Model\Config\Source\Yesno + Activate the extended field pattern function. + + + + Magento\Config\Model\Config\Source\Yesno + + 1 + + Enable or disable pattern validation for city fields. + + + + Magento\Config\Model\Config\Source\Yesno + + 1 + + Enable or disable pattern validation for name fields. + + + + Magento\Config\Model\Config\Source\Yesno + + 1 + + Enable or disable pattern validation for telephone fields. + + + + Magento\Config\Model\Config\Source\Yesno + + 1 + + Enable or disable pattern validation for street fields. + + + + Magento\Config\Model\Config\Source\Yesno + + 1 + + Enable or disable pattern validation for e-mail fields. + + + + + 1 + + Enter one E-Mail or Host per line for banned validation. + + + + Magento\Config\Model\Config\Source\Yesno + Activate the extended pattern function to limit code injection. + + +
diff --git a/app/code/Magento/Customer/etc/validation.xml b/app/code/Magento/Customer/etc/validation.xml index 7fd6cfeb79472..225d22a46ff89 100644 --- a/app/code/Magento/Customer/etc/validation.xml +++ b/app/code/Magento/Customer/etc/validation.xml @@ -1,8 +1,8 @@ @@ -23,12 +23,30 @@ + + + + + + + + + + + + + + + + + + @@ -38,7 +56,6 @@ - @@ -51,9 +68,14 @@ - + - + + + + + + @@ -61,9 +83,14 @@ - + - + + + + + + @@ -72,9 +99,11 @@ + + + - diff --git a/app/code/Magento/Newsletter/Model/SubscriptionManager.php b/app/code/Magento/Newsletter/Model/SubscriptionManager.php index 05be8325e3243..3af18adaad521 100644 --- a/app/code/Magento/Newsletter/Model/SubscriptionManager.php +++ b/app/code/Magento/Newsletter/Model/SubscriptionManager.php @@ -1,7 +1,7 @@ logger = $logger; $this->storeManager = $storeManager; $this->scopeConfig = $scopeConfig; + $this->validatorFactory = $validatorFactory; $this->customerAccountManagement = $customerAccountManagement; $this->customerRepository = $customerRepository; $this->customerSubscriberCache = $customerSubscriberCache @@ -90,6 +100,10 @@ public function __construct( */ public function subscribe(string $email, int $storeId): Subscriber { + if ($email) { + $this->_validate($email); + } + $websiteId = (int)$this->storeManager->getStore($storeId)->getWebsiteId(); $subscriber = $this->subscriberFactory->create()->loadBySubscriberEmail($email, $websiteId); $currentStatus = (int)$subscriber->getStatus(); @@ -111,6 +125,28 @@ public function subscribe(string $email, int $storeId): Subscriber return $subscriber; } + /** + * Validate the subscriber's email for guest subscribers. + * + * @param string $email + * @return void + * @throws ValidatorException + */ + protected function _validate(string $email): void + { + // Create the validator using the entity and group defined in the XML + $validator = $this->validatorFactory->createValidator('newsletter_subscriber', 'save'); + + // Check if the email is valid + if (!$validator->isValid($email)) { + throw new ValidatorException( + null, + null, + $validator->getMessages() + ); + } + } + /** * @inheritdoc */ diff --git a/app/code/Magento/Newsletter/Model/Validator/Email.php b/app/code/Magento/Newsletter/Model/Validator/Email.php new file mode 100644 index 0000000000000..6d695577eaaea --- /dev/null +++ b/app/code/Magento/Newsletter/Model/Validator/Email.php @@ -0,0 +1,93 @@ +emailValidator = $emailValidator; + } + + /** + * Validate email fields. + * + * @param string $email + * @return bool + */ + public function isValid($email): bool + { + if (!$this->emailValidator->isValidationEnabled()) { + return true; + } + + if (!$this->validateEmailField($email)) { + return false; + } + + return count($this->_messages) == 0; + } + + /** + * Validate the email field. + * + * @param string|null $emailValue + * @return bool + */ + private function validateEmailField(?string $emailValue): bool + { + if (!$this->emailValidator->isValid($emailValue)) { + parent::_addMessages( + [ + __( + 'Email address is not valid! Allowed characters: %1', + $this->emailValidator->allowedCharsDescription + ), + ] + ); + return false; + } + + if ($this->isBlacklistEmail($emailValue)) { + parent::_addMessages([ + __('The email address or domain is blacklisted.') + ]); + return false; + } + + return true; + } + + /** + * Check if email field is blacklisted using the EmailAddressValidator. + * + * @param string|null $emailValue + * @return bool + */ + private function isBlacklistEmail(?string $emailValue): bool + { + return $this->emailValidator->isBlacklist($emailValue); + } +} diff --git a/app/code/Magento/Newsletter/etc/validation.xml b/app/code/Magento/Newsletter/etc/validation.xml new file mode 100644 index 0000000000000..2721621d5509c --- /dev/null +++ b/app/code/Magento/Newsletter/etc/validation.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Review/Model/Review.php b/app/code/Magento/Review/Model/Review.php index ef2474637f384..e8f22c8e38da1 100644 --- a/app/code/Magento/Review/Model/Review.php +++ b/app/code/Magento/Review/Model/Review.php @@ -1,7 +1,7 @@ _reviewSummary = $reviewSummary; $this->_storeManager = $storeManager; $this->_urlModel = $urlModel; + $this->validatorFactory = $validatorFactory; parent::__construct($context, $registry, $resource, $resourceCollection, $data); } @@ -292,12 +301,32 @@ public function validate() $errors[] = __('Please enter a review.'); } + $this->_validate(); + if (empty($errors)) { return true; } return $errors; } - + + /** + * Validate review using custom validator. + * + * @throws ValidatorException + */ + protected function _validate() + { + $validator = $this->validatorFactory->createValidator('review', 'save'); + + if (!$validator->isValid($this)) { + throw new ValidatorException( + null, + null, + $validator->getMessages() + ); + } + } + /** * Perform actions after object delete * diff --git a/app/code/Magento/Review/Model/Validator/Email.php b/app/code/Magento/Review/Model/Validator/Email.php new file mode 100644 index 0000000000000..7a66948865705 --- /dev/null +++ b/app/code/Magento/Review/Model/Validator/Email.php @@ -0,0 +1,94 @@ +emailValidator = $emailValidator; + } + + /** + * Validate email fields. + * + * @param string $email + * @return bool + */ + public function isValid($email): bool + { + if (!$this->emailValidator->isValidationEnabled()) { + return true; + } + + if (!$this->validateEmailField($email)) { + return false; + } + + return count($this->_messages) == 0; + } + + /** + * Validate the email field. + * + * @param string|null $emailValue + * @return bool + */ + private function validateEmailField(?string $emailValue): bool + { + if (!$this->emailValidator->isValid($emailValue)) { + parent::_addMessages( + [ + __( + 'Email address is not valid! Allowed characters: %1', + $this->emailValidator->allowedCharsDescription + ), + ] + ); + return false; + } + + if ($this->isBlacklistEmail($emailValue)) { + parent::_addMessages([ + __('The email address or domain is blacklisted.') + ]); + return false; + } + + return true; + } + + /** + * Check if email field is blacklisted using the EmailAddressValidator. + * + * @param string|null $emailValue + * @return bool + */ + private function isBlacklistEmail(?string $emailValue): bool + { + return $this->emailValidator->isBlacklist($emailValue); + } +} diff --git a/app/code/Magento/Review/Model/Validator/ForbiddenPattern.php b/app/code/Magento/Review/Model/Validator/ForbiddenPattern.php new file mode 100644 index 0000000000000..36e9f1f70b6ea --- /dev/null +++ b/app/code/Magento/Review/Model/Validator/ForbiddenPattern.php @@ -0,0 +1,60 @@ +forbiddenValidator = $forbiddenValidator; + } + + /** + * Validate multiple review fields against forbidden patterns. + * + * @param array $values + * @return bool + */ + public function isValid($values): bool + { + if (!$this->forbiddenValidator->isValidationEnabled()) { + return true; + } + + foreach ($values as $field => $value) { + if (empty($value)) { + continue; + } + + if (!$this->forbiddenValidator->isValid($value)) { + parent::_addMessages([ + __("Fraud Protection: Forbidden pattern detected in review field") + ]); + } + } + + return count($this->_messages) == 0; + } +} diff --git a/app/code/Magento/Review/etc/validation.xml b/app/code/Magento/Review/etc/validation.xml new file mode 100644 index 0000000000000..2e39bd21de89e --- /dev/null +++ b/app/code/Magento/Review/etc/validation.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +