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..764dc50b76add --- /dev/null +++ b/vendor/magento/module-customer/Model/Plugin/CustomerRepository/RestrictedFieldsPlugin.php @@ -0,0 +1,69 @@ +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 04dee8d9f6b63..031db90cde668 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 095e41ae851a1..fb103bc3441b6 100644 --- a/vendor/magento/module-customer/i18n/en_US.csv +++ b/vendor/magento/module-customer/i18n/en_US.csv @@ -548,4 +548,6 @@ Addresses,Addresses "Full Match","Full Match" "Prefix Match","Prefix Match" "Something went wrong while logging out customer.","Something went wrong while logging out customer." +"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." "Address not found.","Address not found." 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 ea3a6a9a9c09e..ae5e041a50e14 100644 --- a/vendor/magento/module-email/Model/Template/Filter.php +++ b/vendor/magento/module-email/Model/Template/Filter.php @@ -980,7 +980,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 e873a9a8a933d..3c5db93968776 100644 --- a/vendor/magento/framework/Escaper.php +++ b/vendor/magento/framework/Escaper.php @@ -387,8 +387,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 8246c3dd0344c..280b420209a5d 100644 --- a/vendor/magento/framework/Filter/Template.php +++ b/vendor/magento/framework/Filter/Template.php @@ -18,6 +18,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 constructions filter @@ -102,6 +103,8 @@ class Template implements FilterInterface /** * @var SignatureProvider|null + * @deprecated Signature-based deferral was superseded by out-of-band deferral via DeferredDirectiveRegistry. + * @see DeferredDirectiveRegistry */ private $signatureProvider; @@ -110,6 +113,11 @@ class Template implements FilterInterface */ private $filteringDepthMeter; + /** + * @var DeferredDirectiveRegistry|null + */ + private $deferredDirectiveRegistry; + /** * @param StringUtils $string * @param array $variables @@ -117,6 +125,7 @@ class Template implements FilterInterface * @param VariableResolverInterface|null $variableResolver * @param SignatureProvider|null $signatureProvider * @param FilteringDepthMeter|null $filteringDepthMeter + * @param DeferredDirectiveRegistry|null $deferredDirectiveRegistry */ public function __construct( StringUtils $string, @@ -124,7 +133,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); @@ -138,6 +148,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), @@ -204,35 +217,11 @@ public function filter($value) $this->filteringDepthMeter->descend(); // Processing of template directives. - $templateDirectivesResults = array_unique( - $this->processDirectives($value), - SORT_REGULAR - ); - - $value = $this->applyDirectivesResults($value, $templateDirectivesResults); - - // Processing of deferred directives received from child templates - // or nested directives. - $deferredDirectivesResults = array_unique( - $this->processDirectives($value, true), - SORT_REGULAR - ); - - $value = $this->applyDirectivesResults($value, $deferredDirectivesResults); - - 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 - ); - } - } + $value = $this->resolveDirectives($value); + + // At the root template, resolve directives that child templates deferred to it. + if ($this->filteringDepthMeter->showMark() === 1) { + $value = $this->resolveDeferredDirectives($value); } $value = $this->afterFilter($value); @@ -243,17 +232,50 @@ public function filter($value) } /** - * Processes template directives and returns an array that contains results produced by each directive. + * 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 = str_replace($placeholder, $this->resolveDirectives($directive), $value); + } + + $this->deferredDirectiveRegistry->clear(); + + return $value; + } + + /** + * 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 \Magento\Framework\Exception\LocalizedException */ - private function processDirectives($value, $isSigned = false): array + private function resolveDirectives(string $value): string { $results = []; @@ -266,10 +288,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); @@ -290,7 +308,7 @@ private function processDirectives($value, $isSigned = false): array } } - return $results; + return $this->applyDirectivesResults($value, array_unique($results, SORT_REGULAR)); } /** @@ -328,38 +346,6 @@ private function applyDirectivesResults(string $value, array $results): string return $value; } - /** - * Modifies given regular expression pattern to be able to recognize signed directives. - * - * @param string $pattern - * - * @return string - * - * @throws \Magento\Framework\Exception\LocalizedException - */ - private function embedSignatureIntoPattern(string $pattern): string - { - $signature = $this->signatureProvider->get(); - - $closingDelimiters = [ - '(' => ')', - '{' => '}', - '[' => ']', - '<' => '>' - ]; - - $closingDelimiter = $openingDelimiter = substr(trim($pattern), 0, 1); - - if (array_key_exists($openingDelimiter, $closingDelimiters)) { - $closingDelimiter = $closingDelimiters[$openingDelimiter]; - } - - $pattern = substr_replace($pattern, $signature, strpos($pattern, $openingDelimiter) + 1, 0); - $pattern = substr_replace($pattern, $signature, strrpos($pattern, $closingDelimiter), 0); - - return $pattern; - } - /** * Runs callbacks that have been added to filter content after directive processing is finished. * diff --git a/vendor/magento/framework/Filter/Template/DeferredDirectiveRegistry.php b/vendor/magento/framework/Filter/Template/DeferredDirectiveRegistry.php new file mode 100644 index 0000000000000..cc33f04824b77 --- /dev/null +++ b/vendor/magento/framework/Filter/Template/DeferredDirectiveRegistry.php @@ -0,0 +1,102 @@ + 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 = []; + } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->directives = []; + $this->salt = null; + } +} diff --git a/vendor/magento/framework/Filter/Template/SignatureProvider.php b/vendor/magento/framework/Filter/Template/SignatureProvider.php index be87b40d9caed..2dc4c24c2a8aa 100644 --- a/vendor/magento/framework/Filter/Template/SignatureProvider.php +++ b/vendor/magento/framework/Filter/Template/SignatureProvider.php @@ -15,6 +15,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 implements ResetAfterRequestInterface { 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 dd402cba79155..6b77f0d37cc04 100644 --- a/vendor/magento/framework/Validator/EmailAddress.php +++ b/vendor/magento/framework/Validator/EmailAddress.php @@ -9,6 +9,13 @@ class EmailAddress extends LaminasEmailAddress implements ValidatorInterface { + /** + * Quoted Pattern + * + * @var string + */ + private const QUOTED_LOCAL_PART_PATTERN = '/^"/'; + /** * @var string[] */ @@ -55,4 +62,18 @@ public function setValidateTld(bool $shouldValidate) { $this->getHostnameValidator()->setOptions(['useTldCheck' => $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 b1cad5bedc07e..e41292f2ca89e 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 c813297bf252d..e395422782950 100644 --- a/vendor/magento/framework/View/Element/UiComponent/DataProvider/Sanitizer.php +++ b/vendor/magento/framework/View/Element/UiComponent/DataProvider/Sanitizer.php @@ -50,7 +50,7 @@ public function sanitize(array $data): array } elseif (!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 its rendering @@ -65,6 +65,17 @@ public function sanitize(array $data): array 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. *