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 ad400620af816..6ebb777f0bb44 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 e2a608f73c528..7f13ba7584dd4 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 f949e1f5aa1c2..a92555cb2e7d7 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 ceca494c480e8..7d3acfc7fccdb 100644 --- a/vendor/magento/module-customer/etc/graphql/di.xml +++ b/vendor/magento/module-customer/etc/graphql/di.xml @@ -19,4 +19,7 @@ + + + diff --git a/vendor/magento/module-customer/etc/webapi_rest/di.xml b/vendor/magento/module-customer/etc/webapi_rest/di.xml index 033321ed9541c..00deea97cf69e 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 c71a1ab71174d..f5e5843db528e 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 308cb9b05cb0e..b8c5e2a32bb54 100644 --- a/vendor/magento/module-customer/i18n/en_US.csv +++ b/vendor/magento/module-customer/i18n/en_US.csv @@ -551,4 +551,6 @@ T,T F,F VAT,VAT "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 7c5c973c60555..7c48928df3c9e 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. @@ -168,6 +170,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 @@ -183,6 +190,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) */ @@ -200,7 +208,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; @@ -216,6 +225,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); } @@ -772,7 +783,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()), @@ -780,27 +791,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 f86deacad3b4e..70248cec31d6e 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 c2a6be533e8ea..cdc03115e443b 100644 --- a/vendor/magento/module-persistent/etc/di.xml +++ b/vendor/magento/module-persistent/etc/di.xml @@ -25,6 +25,9 @@ + + + 500 diff --git a/vendor/magento/framework/Escaper.php b/vendor/magento/framework/Escaper.php index 26a1a57adc90b..a1bc7b34473e8 100644 --- a/vendor/magento/framework/Escaper.php +++ b/vendor/magento/framework/Escaper.php @@ -389,8 +389,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 3476aceba1217..2ee66c206d835 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 09a6c5b430555..87adea0bf03c5 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 69ffa04be8e70..83a19a7043c77 100644 --- a/vendor/magento/framework/Lock/Backend/FileLock.php +++ b/vendor/magento/framework/Lock/Backend/FileLock.php @@ -133,7 +133,7 @@ public function cleanupOldLocks(): int continue; } - if ($this->isLocked(basename($lockFile))) { + if ($this->isLockFileActive($lockFile)) { continue; } @@ -157,25 +157,7 @@ public function cleanupOldLocks(): int */ public function isLocked(string $name): bool { - $lockFile = $this->getLockPath($name); - $result = false; - - try { - if ($this->fileDriver->isExists($lockFile)) { - $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); - if ($this->tryToLock($fileResource)) { - $result = false; - } else { - $result = true; - } - $this->tryToUnlock($fileResource); - $this->fileDriver->fileClose($fileResource); - } - } catch (FileSystemException $exception) { - throw new RuntimeException(new Phrase('Cannot verify that the lock exists.'), $exception); - } - - return $result; + return $this->isFileLocked($this->getLockPath($name)); } /** @@ -204,7 +186,51 @@ public function unlock(string $name): bool */ private function getLockPath(string $name): string { - return $this->path . $name; + return $this->path . hash('sha256', $name); + } + + /** + * Checks whether a lock file at the given absolute path is currently held + * + * @param string $lockFile Absolute path to the lock file + * @return bool + * @throws RuntimeException + */ + private function isFileLocked(string $lockFile): bool + { + $result = false; + + try { + if ($this->fileDriver->isExists($lockFile)) { + $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); + if ($this->tryToLock($fileResource)) { + $result = false; + } else { + $result = true; + } + $this->tryToUnlock($fileResource); + $this->fileDriver->fileClose($fileResource); + } + } catch (FileSystemException $exception) { + throw new RuntimeException(new Phrase('Cannot verify that the lock exists.'), $exception); + } + + return $result; + } + + /** + * Checks whether a lock file at the given absolute path is actively held. + * + * @param string $lockFilePath Absolute path to the lock file + * @return bool + */ + private function isLockFileActive(string $lockFilePath): bool + { + try { + return $this->isFileLocked($lockFilePath); + } catch (RuntimeException) { + return true; + } } /** diff --git a/vendor/magento/framework/Validator/EmailAddress.php b/vendor/magento/framework/Validator/EmailAddress.php index 1a607f129877c..ddb35bb07d978 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 f3b3a6bc9216e..2061228d568cc 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 3ac2c9135ea09..7d9d4577a22a3 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. *