diff --git a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php index f71058a71519f..824b3f039973b 100644 --- a/vendor/magento/module-catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php +++ b/vendor/magento/module-catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php @@ -165,37 +165,34 @@ public function execute() */ private function sanitizeProductAttributes($attributesData) { + $normalised = []; foreach ($attributesData as $attributeCode => $value) { if ($attributeCode === ProductAttributeInterface::CODE_HAS_WEIGHT) { + $normalised[$attributeCode] = $value; continue; } - $attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); - if (!$attribute->getAttributeId()) { - unset($attributesData[$attributeCode]); continue; } + $canonicalCode = $attribute->getAttributeCode(); if ($attribute->getBackendType() === 'datetime') { - $attributesData[$attributeCode] = $this->filterDate( + $value = $this->filterDate( $value, $attribute->getFrontendInput() === 'datetime' ); } elseif ($attribute->getFrontendInput() === 'multiselect') { - // Check if 'Change' checkbox has been checked by admin for this attribute - $isChanged = (bool)$this->getRequest()->getPost('toggle_' . $attributeCode); + $isChanged = (bool)$this->getRequest()->getPost('toggle_' . $canonicalCode); if (!$isChanged) { - unset($attributesData[$attributeCode]); continue; } - if (is_array($value)) { - $value = implode(',', $value); - } - $attributesData[$attributeCode] = $value; + $value = is_array($value) ? implode(',', $value) : $value; } + + $normalised[$canonicalCode] = $value; } - return $attributesData; + return $normalised; } /** diff --git a/vendor/magento/module-customer/Model/ForgotPasswordToken/GetCustomerByToken.php b/vendor/magento/module-customer/Model/ForgotPasswordToken/GetCustomerByToken.php index 994bf7b40deb5..e91bab987e906 100644 --- a/vendor/magento/module-customer/Model/ForgotPasswordToken/GetCustomerByToken.php +++ b/vendor/magento/module-customer/Model/ForgotPasswordToken/GetCustomerByToken.php @@ -9,15 +9,19 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\State\ExpiredException; use Magento\Framework\Phrase; /** * Get Customer By reset password token + * * @SuppressWarnings(PHPMD.LongVariable) * @deprecated Rp Tokens cannot be looked up directly + * @see not used anymore */ class GetCustomerByToken { @@ -32,17 +36,24 @@ class GetCustomerByToken private $searchCriteriaBuilder; /** - * ForgotPassword constructor. - * + * @var CollectionFactory + */ + private $collectionFactory; + + /** * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder + * @param CollectionFactory|null $collectionFactory */ public function __construct( CustomerRepositoryInterface $customerRepository, - SearchCriteriaBuilder $searchCriteriaBuilder + SearchCriteriaBuilder $searchCriteriaBuilder, + ?CollectionFactory $collectionFactory = null ) { $this->customerRepository = $customerRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->collectionFactory = $collectionFactory + ?? ObjectManager::getInstance()->get(CollectionFactory::class); } /** @@ -53,38 +64,27 @@ public function __construct( * @return \Magento\Customer\Api\Data\CustomerInterface * @throws ExpiredException * @throws NoSuchEntityException - * @throws \Magento\Framework\Exception\LocalizedException */ public function execute(string $resetPasswordToken): CustomerInterface { - $this->searchCriteriaBuilder->addFilter( - 'rp_token', - $resetPasswordToken - ); - $this->searchCriteriaBuilder->setPageSize(1); - $found = $this->customerRepository->getList( - $this->searchCriteriaBuilder->create() - ); + $collection = $this->collectionFactory->create(); + $collection->addAttributeToFilter('rp_token', $resetPasswordToken); + $size = $collection->getSize(); - if ($found->getTotalCount() > 1) { - //Failed to generated unique RP token + if ($size > 1) { throw new ExpiredException( new Phrase('Reset password token expired.') ); } - if ($found->getTotalCount() === 0) { - //Customer with such token not found. + if ($size === 0) { throw new NoSuchEntityException( new Phrase( 'No such entity with rp_token = %value', - [ - 'value' => $resetPasswordToken - ] + ['value' => $resetPasswordToken] ) ); } - //Unique customer found. - return $found->getItems()[0]; + return $collection->getFirstItem(); } } diff --git a/vendor/magento/module-customer/Model/Plugin/CustomerRepository/RestrictedFieldsPlugin.php b/vendor/magento/module-customer/Model/Plugin/CustomerRepository/RestrictedFieldsPlugin.php new file mode 100644 index 0000000000000..22b15905b4934 --- /dev/null +++ b/vendor/magento/module-customer/Model/Plugin/CustomerRepository/RestrictedFieldsPlugin.php @@ -0,0 +1,74 @@ +restrictedFields = $restrictedFields; + } + + /** + * Check forbidden customer fields + * + * @param CustomerRepositoryInterface $subject + * @param SearchCriteriaInterface $searchCriteria + * @return array + * @throws InputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeGetList( + CustomerRepositoryInterface $subject, + SearchCriteriaInterface $searchCriteria + ): array { + foreach ($searchCriteria->getFilterGroups() as $filterGroup) { + foreach ($filterGroup->getFilters() as $filter) { + if ($this->isRestricted($filter->getField())) { + throw new InputException( + __('The search criteria filter field is invalid.') + ); + } + } + } + foreach ($searchCriteria->getSortOrders() ?? [] as $sortOrder) { + if ($this->isRestricted($sortOrder->getField())) { + throw new InputException( + __('The search criteria sort field is invalid.') + ); + } + } + return [$searchCriteria]; + } + + /** + * Check if the given field name matches a restricted field + * + * @param string|null $field + * @return bool + */ + private function isRestricted(?string $field): bool + { + return $field !== null && in_array(strtolower($field), $this->restrictedFields, true); + } +} diff --git a/vendor/magento/module-customer/etc/di.xml b/vendor/magento/module-customer/etc/di.xml index 120a8dda8aece..c9dc21a0faa06 100644 --- a/vendor/magento/module-customer/etc/di.xml +++ b/vendor/magento/module-customer/etc/di.xml @@ -85,6 +85,15 @@ SessionValidator + + + + password_hash + rp_token + confirmation + + + Magento\Customer\Model\Address\Config\Proxy diff --git a/vendor/magento/module-customer/etc/graphql/di.xml b/vendor/magento/module-customer/etc/graphql/di.xml index e5ed4078b4a04..89095704f51e8 100644 --- a/vendor/magento/module-customer/etc/graphql/di.xml +++ b/vendor/magento/module-customer/etc/graphql/di.xml @@ -16,4 +16,7 @@ + + + diff --git a/vendor/magento/module-customer/etc/webapi_rest/di.xml b/vendor/magento/module-customer/etc/webapi_rest/di.xml index 18627b68320ed..f649cb6732b94 100644 --- a/vendor/magento/module-customer/etc/webapi_rest/di.xml +++ b/vendor/magento/module-customer/etc/webapi_rest/di.xml @@ -21,6 +21,7 @@ + diff --git a/vendor/magento/module-customer/etc/webapi_soap/di.xml b/vendor/magento/module-customer/etc/webapi_soap/di.xml index cb0b1ce58a594..2f9c0cc786995 100644 --- a/vendor/magento/module-customer/etc/webapi_soap/di.xml +++ b/vendor/magento/module-customer/etc/webapi_soap/di.xml @@ -18,6 +18,9 @@ + + + diff --git a/vendor/magento/module-customer/i18n/en_US.csv b/vendor/magento/module-customer/i18n/en_US.csv index a00bb04523ccc..581114c2dc4b5 100644 --- a/vendor/magento/module-customer/i18n/en_US.csv +++ b/vendor/magento/module-customer/i18n/en_US.csv @@ -546,3 +546,5 @@ Addresses,Addresses "The specified customer group id does not exist.","The specified customer group id does not exist." "Something went wrong while logging out customer.","Something went wrong while logging out customer." "Address not found.","Address not found." +"The search criteria filter field is invalid.","The search criteria filter field is invalid." +"The search criteria sort field is invalid.","The search criteria sort field is invalid." diff --git a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php index d2fdcb8e315fe..f234ce8312297 100644 --- a/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php +++ b/vendor/magento/module-email/Block/Adminhtml/Template/Preview.php @@ -7,6 +7,10 @@ namespace Magento\Email\Block\Adminhtml\Template; +use Magento\Email\Model\Template\Validator as TemplateValidator; +use Magento\Framework\Validation\ValidationException; +use Magento\Framework\App\ObjectManager; + /** * Email template preview block. * @@ -30,20 +34,29 @@ class Preview extends \Magento\Backend\Block\Widget */ protected $profilerName = 'email_template_proccessing'; + /** + * @var TemplateValidator + */ + private $templateValidator; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Filter\Input\MaliciousCode $maliciousCode * @param \Magento\Email\Model\TemplateFactory $emailFactory * @param array $data + * @param TemplateValidator $templateValidator */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Filter\Input\MaliciousCode $maliciousCode, \Magento\Email\Model\TemplateFactory $emailFactory, - array $data = [] + array $data = [], + ?TemplateValidator $templateValidator = null ) { $this->_maliciousCode = $maliciousCode; $this->_emailFactory = $emailFactory; + $this->templateValidator = $templateValidator ?: + ObjectManager::getInstance()->get(TemplateValidator::class); parent::__construct($context, $data); } @@ -64,10 +77,23 @@ protected function _toHtml() if ($id = (int)$request->getParam('id')) { $template->load($id); } else { + $rawText = $request->getParam('text'); + $rawText = is_string($rawText) ? $rawText : ''; + $rawStyles = $request->getParam('styles'); + $rawStyles = is_string($rawStyles) ? $rawStyles : ''; + try { + $this->templateValidator->validate($rawText); + if ($rawStyles !== '') { + $this->templateValidator->validate($rawStyles); + } + } catch (ValidationException $e) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Template content validation failed. Please review template syntax.') + ); + } $template->setTemplateType($request->getParam('type')); - $template->setTemplateText($this->_maliciousCode->filter($request->getParam('text'))); - $template->setTemplateStyles($request->getParam('styles')); - + $template->setTemplateText($this->_maliciousCode->filter($rawText)); + $template->setTemplateStyles($rawStyles); } \Magento\Framework\Profiler::start($this->profilerName); diff --git a/vendor/magento/module-email/Model/AbstractTemplate.php b/vendor/magento/module-email/Model/AbstractTemplate.php index a3e328bb2eb30..dd037905ae732 100644 --- a/vendor/magento/module-email/Model/AbstractTemplate.php +++ b/vendor/magento/module-email/Model/AbstractTemplate.php @@ -16,6 +16,8 @@ use Magento\Store\Model\Store; use Magento\MediaStorage\Helper\File\Storage\Database; use Magento\Framework\Validation\ValidationException; +use Magento\Email\Model\Template\Validator as TemplateValidator; +use Magento\Framework\App\ObjectManager; /** * Template model class. @@ -169,6 +171,11 @@ abstract class AbstractTemplate extends AbstractModel implements TemplateTypesIn */ private $fileStorageDatabase; + /** + * @var TemplateValidator + */ + private $templateValidator; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\View\DesignInterface $design @@ -184,6 +191,7 @@ abstract class AbstractTemplate extends AbstractModel implements TemplateTypesIn * @param \Magento\Framework\UrlInterface $urlModel * @param array $data * @param Database $fileStorageDatabase + * @param TemplateValidator|null $templateValidator * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -201,7 +209,8 @@ public function __construct( \Magento\Framework\Filter\FilterManager $filterManager, \Magento\Framework\UrlInterface $urlModel, array $data = [], - Database $fileStorageDatabase = null + Database $fileStorageDatabase = null, + ?TemplateValidator $templateValidator = null ) { $this->design = $design; $this->area = isset($data['area']) ? $data['area'] : null; @@ -217,6 +226,8 @@ public function __construct( $this->urlModel = $urlModel; $this->fileStorageDatabase = $fileStorageDatabase ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Database::class); + $this->templateValidator = $templateValidator ?: + ObjectManager::getInstance()->get(TemplateValidator::class); parent::__construct($context, $registry, null, null, $data); } @@ -773,7 +784,7 @@ public function beforeSave() $templateText = $this->getTemplateText(); if ($templateText) { try { - $this->validateTemplateText($templateText); + $this->templateValidator->validate($templateText); } catch (ValidationException $exception) { throw new ValidationException( __('Content field contains restricted HTML elements. %1', $exception->getMessage()), @@ -781,27 +792,18 @@ public function beforeSave() ); } } + $templateStyles = $this->getTemplateStyles(); + if ($templateStyles) { + try { + $this->templateValidator->validate($templateStyles); + } catch (ValidationException $exception) { + throw new ValidationException( + __('Styles field validation failed. %1', $exception->getMessage()), + $exception + ); + } + } parent::beforeSave(); return $this; } - - /** - * Validate template text - * - * @param string $content - * @throws ValidationException - */ - private function validateTemplateText(string $content): void - { - $expression = '/\b(?:src|href|action)\s*=\s*([\'"])(?:[^\'"]*?)https?:\/\/[^\/\s]+\/' . - '(?:[^\'"]*?)\{\{\s*[^}]+\s*\}\}(?:[^\'"]*?)\1/iu'; - - if (preg_match($expression, $content)) { - throw new ValidationException( - __( - 'Template directives are not allowed in external URLs.' - ) - ); - } - } } diff --git a/vendor/magento/module-email/Model/Template/Filter.php b/vendor/magento/module-email/Model/Template/Filter.php index 2a11376ae03fd..b6ddcd425e3ef 100644 --- a/vendor/magento/module-email/Model/Template/Filter.php +++ b/vendor/magento/module-email/Model/Template/Filter.php @@ -967,7 +967,14 @@ public function inlinecssDirective($construction) // If this template is a child of another template, skip processing so that the parent template will process // this directive. This is important as CSS inlining must operate on the entire HTML document. if ($this->isChildTemplate()) { - return $construction[0]; + try { + return $this->deferToParent($construction[0]); + } catch (\Exception $e) { + throw new MailException( + __('Unable to defer the "inlinecss" directive for later processing.'), + $e + ); + } } $params = $this->getParameters($construction[2]); diff --git a/vendor/magento/module-email/Model/Template/Validator.php b/vendor/magento/module-email/Model/Template/Validator.php new file mode 100644 index 0000000000000..3173d7a57d8ab --- /dev/null +++ b/vendor/magento/module-email/Model/Template/Validator.php @@ -0,0 +1,96 @@ +`])+))/iu'; + + /** + * Regular expressions for CSS url argument containing a template directive + * + * @var string + */ + private const CSS_URL_VALUE_EXPRESSION = + '/\burl\(\s*(?|"([^"]*\{\{[^}]*\}\}[^"]*)"' . + '|\'([^\']*\{\{[^}]*\}\}[^\']*)\'' . + '|((?:\{\{[^}]*\}\}|[^\s"\')])+))\s*\)/iu'; + + /** + * Validate template content + * + * @param string $content + * + * @throws ValidationException + */ + public function validate(string $content): void + { + foreach ([self::ATTRIBUTE_VALUE_EXPRESSION, self::CSS_URL_VALUE_EXPRESSION] as $expression) { + $matchCount = preg_match_all($expression, $content, $matches); + if ($matchCount === false) { + throw new ValidationException( + __('Template content validation failed. Please review template syntax.') + ); + } + foreach ($matches[1] as $value) { + if ($this->isForbiddenUrlValue($value)) { + throw new ValidationException( + __('Template content validation failed. Please review template syntax.') + ); + } + } + } + } + + /** + * Check whether a URL-bearing value resolves to an absolute external target + * + * @param string $value + * @return bool + */ + private function isForbiddenUrlValue(string $value): bool + { + $normalized = $this->normalizeUrlValue($value); + if (strpos($normalized, '{{') === false) { + return false; + } + + return preg_match('#^https?:/#i', $normalized) === 1 + || strncmp($normalized, '//', 2) === 0; + } + + /** + * Normalize a URL-bearing value + * + * @param string $value + * @return string + */ + private function normalizeUrlValue(string $value): string + { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $value = str_ireplace(['%2f', '%5c'], ['/', '\\'], $value); + $value = str_replace('\\', '/', $value); + + return preg_replace('/[\x00-\x20]+/', '', $value) ?? $value; + } +} diff --git a/vendor/magento/module-persistent/Model/Plugin/AllowPersistentCustomerGuestCart.php b/vendor/magento/module-persistent/Model/Plugin/AllowPersistentCustomerGuestCart.php new file mode 100644 index 0000000000000..5ba173320b2ad --- /dev/null +++ b/vendor/magento/module-persistent/Model/Plugin/AllowPersistentCustomerGuestCart.php @@ -0,0 +1,58 @@ +persistentHelper->isShoppingCartPersist() + && !$this->customerSession->isLoggedIn() + && $this->persistentSession->isPersistent() + && (int) $this->persistentSession->getSession()->getCustomerId() === $customerId; + + if (!$isOwnedByPersistedCustomer) { + $proceed($customerId, $maskedCartId); + } + } +} diff --git a/vendor/magento/module-persistent/etc/di.xml b/vendor/magento/module-persistent/etc/di.xml index fd1c97fae66d9..efa49a4053bea 100644 --- a/vendor/magento/module-persistent/etc/di.xml +++ b/vendor/magento/module-persistent/etc/di.xml @@ -22,4 +22,7 @@ Magento\Checkout\Model\Session\Proxy + + + diff --git a/vendor/magento/framework/Escaper.php b/vendor/magento/framework/Escaper.php index 7e608b7719c3e..09bebe8f13611 100644 --- a/vendor/magento/framework/Escaper.php +++ b/vendor/magento/framework/Escaper.php @@ -369,8 +369,9 @@ public function escapeJsQuote($data, $quote = '\'') */ public function escapeXssInUrl($data) { - $data = $this->decodeHtmlEntitiesToFixedPoint((string)$data); + $data = (string)$data; $this->getTranslateInline()->processResponseBody($data); + $data = $this->decodeHtmlEntitiesToFixedPoint($data); return htmlspecialchars( $this->escapeScriptIdentifiers($data), diff --git a/vendor/magento/framework/Filter/Template.php b/vendor/magento/framework/Filter/Template.php index 39bf4c23e16e3..c2d8214ac3c22 100644 --- a/vendor/magento/framework/Filter/Template.php +++ b/vendor/magento/framework/Filter/Template.php @@ -21,6 +21,7 @@ use Magento\Framework\Stdlib\StringUtils; use Magento\Framework\Filter\Template\SignatureProvider; use Magento\Framework\Filter\Template\FilteringDepthMeter; +use Magento\Framework\Filter\Template\DeferredDirectiveRegistry; /** * Template filter @@ -105,6 +106,8 @@ class Template implements \Zend_Filter_Interface /** * @var SignatureProvider|null + * @deprecated Signature-based deferral was superseded by out-of-band deferral via DeferredDirectiveRegistry. + * @see DeferredDirectiveRegistry */ private $signatureProvider; @@ -113,6 +116,11 @@ class Template implements \Zend_Filter_Interface */ private $filteringDepthMeter; + /** + * @var DeferredDirectiveRegistry|null + */ + private $deferredDirectiveRegistry; + /** * @param StringUtils $string * @param array $variables @@ -120,6 +128,7 @@ class Template implements \Zend_Filter_Interface * @param VariableResolverInterface|null $variableResolver * @param SignatureProvider|null $signatureProvider * @param FilteringDepthMeter|null $filteringDepthMeter + * @param DeferredDirectiveRegistry|null $deferredDirectiveRegistry */ public function __construct( StringUtils $string, @@ -127,7 +136,8 @@ public function __construct( $directiveProcessors = [], VariableResolverInterface $variableResolver = null, SignatureProvider $signatureProvider = null, - FilteringDepthMeter $filteringDepthMeter = null + FilteringDepthMeter $filteringDepthMeter = null, + DeferredDirectiveRegistry $deferredDirectiveRegistry = null ) { $this->string = $string; $this->setVariables($variables); @@ -141,6 +151,9 @@ public function __construct( $this->filteringDepthMeter = $filteringDepthMeter ?? ObjectManager::getInstance() ->get(FilteringDepthMeter::class); + $this->deferredDirectiveRegistry = $deferredDirectiveRegistry ?? ObjectManager::getInstance() + ->get(DeferredDirectiveRegistry::class); + if (empty($directiveProcessors)) { $this->directiveProcessors = [ 'depend' => ObjectManager::getInstance()->get(DependDirective::class), @@ -206,54 +219,65 @@ public function filter($value) $this->filteringDepthMeter->descend(); // Processing of template directives. - $templateDirectivesResults = $this->processDirectives($value); + $value = $this->resolveDirectives($value); - foreach ($templateDirectivesResults as $result) { - $value = str_replace($result['directive'], $result['output'], $value); + // At the root template, resolve directives that child templates deferred to it. + if ($this->filteringDepthMeter->showMark() === 1) { + $value = $this->resolveDeferredDirectives($value); } - // Processing of deferred directives received from child templates - // or nested directives. - $deferredDirectivesResults = $this->processDirectives($value, true); + $value = $this->afterFilter($value); - foreach ($deferredDirectivesResults as $result) { - $value = str_replace($result['directive'], $result['output'], $value); - } + $this->filteringDepthMeter->ascend(); - if ($this->filteringDepthMeter->showMark() > 1) { - // Signing own deferred directives (if any). - $signature = $this->signatureProvider->get(); - - foreach ($templateDirectivesResults as $result) { - if ($result['directive'] === $result['output']) { - $value = str_replace( - $result['output'], - $signature . $result['output'] . $signature, - $value - ); - } + return $value; + } + + /** + * Defers a directive to the root template and returns the placeholder that represents it. + * + * @param string $directive + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function deferToParent(string $directive): string + { + return $this->deferredDirectiveRegistry->defer($directive); + } + + /** + * Resolves directives that child templates deferred to the root template. + * + * @param string $value + * @return string + * @throws \Exception + */ + private function resolveDeferredDirectives(string $value): string + { + foreach ($this->deferredDirectiveRegistry->getAll() as $placeholder => $directive) { + if (strpos($value, $placeholder) === false) { + continue; } - } - $value = $this->afterFilter($value); + $value = str_replace($placeholder, $this->resolveDirectives($directive), $value); + } - $this->filteringDepthMeter->ascend(); + $this->deferredDirectiveRegistry->clear(); return $value; } /** - * Processes template directives and returns an array that contains results produced by each directive. + * Processes directives and applies the results produced by each directive to the given value. * * @param string $value - * @param bool $isSigned * - * @return array + * @return string * * @throws InvalidArgumentException * @throws LocalizedException */ - private function processDirectives($value, $isSigned = false): array + private function resolveDirectives(string $value): string { $results = []; @@ -266,10 +290,6 @@ private function processDirectives($value, $isSigned = false): array $pattern = $directiveProcessor->getRegularExpression(); - if ($isSigned) { - $pattern = $this->embedSignatureIntoPattern($pattern); - } - if (preg_match_all($pattern, $value, $constructions, PREG_SET_ORDER)) { foreach ($constructions as $construction) { $replacedValue = $directiveProcessor->process($construction, $this, $this->templateVars); @@ -282,39 +302,42 @@ private function processDirectives($value, $isSigned = false): array } } - return $results; + return $this->applyDirectivesResults($value, array_unique($results, SORT_REGULAR)); } /** - * Modifies given regular expression pattern to be able to recognize signed directives. + * Applies results produced by directives. * - * @param string $pattern + * @param string $value + * @param array $results * * @return string - * - * @throws LocalizedException */ - private function embedSignatureIntoPattern(string $pattern): string + private function applyDirectivesResults(string $value, array $results): string { - $signature = $this->signatureProvider->get(); + $processedResults = []; + + foreach ($results as $result) { + foreach ($processedResults as $processedResult) { + $result['directive'] = str_replace( + $processedResult['directive'], + $processedResult['output'], + $result['directive'] + ); + } - $closingDelimiters = [ - '(' => ')', - '{' => '}', - '[' => ']', - '<' => '>' - ]; + $value = str_replace($result['directive'], $result['output'], $value); - $closingDelimiter = $openingDelimiter = substr(trim($pattern), 0, 1); + if (isset($result['callbacks'])) { + foreach ($result['callbacks'] as $callback) { + $this->addAfterFilterCallback($callback); + } + } - if (array_key_exists($openingDelimiter, $closingDelimiters)) { - $closingDelimiter = $closingDelimiters[$openingDelimiter]; + $processedResults[] = $result; } - $pattern = substr_replace($pattern, $signature, strpos($pattern, $openingDelimiter) + 1, 0); - $pattern = substr_replace($pattern, $signature, strrpos($pattern, $closingDelimiter), 0); - - return $pattern; + return $value; } /** diff --git a/vendor/magento/framework/Filter/Template/DeferredDirectiveRegistry.php b/vendor/magento/framework/Filter/Template/DeferredDirectiveRegistry.php new file mode 100644 index 0000000000000..c80b0e1848c51 --- /dev/null +++ b/vendor/magento/framework/Filter/Template/DeferredDirectiveRegistry.php @@ -0,0 +1,92 @@ + deferred directive text. + * + * @var string[] + */ + private array $directives = []; + + /** + * Per-request secret salt used to derive placeholder tokens deterministically. + * + * @var string|null + */ + private ?string $salt = null; + + /** + * @param Random $random + */ + public function __construct(Random $random) + { + $this->random = $random; + } + + /** + * Register a directive as deferred and return the placeholder token that represents it. + * + * @param string $directive + * @return string + * @throws LocalizedException + */ + public function defer(string $directive): string + { + if ($this->salt === null) { + $this->salt = $this->random->getRandomString(self::TOKEN_LENGTH); + } + + $placeholder = ':deferred:' . hash('sha256', $this->salt . $directive) . ':'; + + $this->directives[$placeholder] = $directive; + + return $placeholder; + } + + /** + * Get all registered deferrals keyed by their placeholder token. + * + * @return string[] + */ + public function getAll(): array + { + return $this->directives; + } + + /** + * Forget all registered deferrals. + * + * @return void + */ + public function clear(): void + { + $this->directives = []; + } +} diff --git a/vendor/magento/framework/Filter/Template/SignatureProvider.php b/vendor/magento/framework/Filter/Template/SignatureProvider.php index 3e476f3e5d79e..5a12ed78c5f1e 100644 --- a/vendor/magento/framework/Filter/Template/SignatureProvider.php +++ b/vendor/magento/framework/Filter/Template/SignatureProvider.php @@ -13,6 +13,10 @@ * Provides a signature which should be used to sign deferred directives * (directives that should be processed in scope of a parent template * instead of own scope, e.g. {{inlinecss}}). + * + * @deprecated Signature-based deferral was superseded by out-of-band deferral via + * \Magento\Framework\Filter\Template\DeferredDirectiveRegistry. + * @see \Magento\Framework\Filter\Template\DeferredDirectiveRegistry */ class SignatureProvider { diff --git a/vendor/magento/framework/Lock/Backend/FileLock.php b/vendor/magento/framework/Lock/Backend/FileLock.php index dd8eda9621667..08b949f56ad9f 100644 --- a/vendor/magento/framework/Lock/Backend/FileLock.php +++ b/vendor/magento/framework/Lock/Backend/FileLock.php @@ -161,7 +161,7 @@ public function unlock(string $name): bool */ private function getLockPath(string $name): string { - return $this->path . $name; + return $this->path . hash('sha256', $name); } /** diff --git a/vendor/magento/framework/Validator/EmailAddress.php b/vendor/magento/framework/Validator/EmailAddress.php index e1eb12be4f652..45a26cc600d16 100644 --- a/vendor/magento/framework/Validator/EmailAddress.php +++ b/vendor/magento/framework/Validator/EmailAddress.php @@ -9,6 +9,13 @@ class EmailAddress extends \Zend_Validate_EmailAddress implements \Magento\Framework\Validator\ValidatorInterface { + /** + * Quoted Pattern + * + * @var string + */ + private const QUOTED_LOCAL_PART_PATTERN = '/^"/'; + /** * Instantiates hostname validator for local use. * TLD validation is off by default. @@ -38,4 +45,18 @@ public function setValidateTld(bool $shouldValidate) { $this->getHostnameValidator()->setValidateTld($shouldValidate); } + + /** + * @inheritdoc + */ + public function isValid($value) + { + if (is_string($value) && preg_match(self::QUOTED_LOCAL_PART_PATTERN, $value)) { + $this->_setValue($value); + $this->_error(self::INVALID_FORMAT); + return false; + } + + return parent::isValid($value); + } } diff --git a/vendor/magento/framework/Validator/HTML/ConfigurableWYSIWYGValidator.php b/vendor/magento/framework/Validator/HTML/ConfigurableWYSIWYGValidator.php index a401899e1052c..e28d4cd774c33 100644 --- a/vendor/magento/framework/Validator/HTML/ConfigurableWYSIWYGValidator.php +++ b/vendor/magento/framework/Validator/HTML/ConfigurableWYSIWYGValidator.php @@ -24,12 +24,19 @@ class ConfigurableWYSIWYGValidator implements WYSIWYGValidatorInterface . '(\\\\x64\\\\x61\\\\x74\\\\x61(\\\\x3a|:|%3A)))/i'; /** - * Pattern to detect malformed syntax + * Pattern to detect malformed or unsupported markup syntax. * * @var string */ private static string $contentFiltrationPattern = - '#<\w+[^>]*/[^\s/>]+/[^>]*=|<\w+/[^\s>]*on\w+\s*=|<\w+[^>]*//[^>]*=|(' + . '|<\s*<[a-zA-Z]' + . '|\x00' + . '|<\w+[^>]*/[^\s/>]+/[^>]*=' + . '|<\w+/[^\s>]*on\w+\s*=' + . '|<\w+[^>]*//[^>]*=' + . '|(? 1; - $hasOtherMalicious = count($matches[0]) > 0 && !isset($valueCounts['loadHTML($content, LIBXML_HTML_NOIMPLIED); restore_error_handler(); diff --git a/vendor/magento/framework/View/Element/UiComponent/DataProvider/Sanitizer.php b/vendor/magento/framework/View/Element/UiComponent/DataProvider/Sanitizer.php index d49e83d1a99e0..be9a9f86a2c18 100644 --- a/vendor/magento/framework/View/Element/UiComponent/DataProvider/Sanitizer.php +++ b/vendor/magento/framework/View/Element/UiComponent/DataProvider/Sanitizer.php @@ -54,7 +54,7 @@ function ($datum, string $key) use (&$config, &$toProcess) : void { !is_bool($config) && !array_key_exists($key, $config) ) && (is_string($datum) || $datum instanceof Phrase) - && preg_match('/\$\{.+\}/s', (string)$datum) + && $this->validateSyntax((string)$datum) ) { //Templating is not disabled for all properties or for this property specifically //Property is a string that contains template syntax so we are disabling it's rendering @@ -74,6 +74,17 @@ function ($datum, string $key) use (&$config, &$toProcess) : void { return $data; } + /** + * Validate value syntax. + * + * @param string $value + * @return bool + */ + private function validateSyntax(string $value): bool + { + return strpos($value, '${') !== false && strpos($value, '${{') === false; + } + /** * Sanitize a component's metadata. *