Skip to content

Commit 521abd7

Browse files
committed
AC-11004: Exception on Storefront when Admin adds CustomerCustomAttribute block via CMS Page Content
1 parent e4a14e0 commit 521abd7

File tree

5 files changed

+268
-0
lines changed

5 files changed

+268
-0
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Cms\Model\Validator;
9+
10+
class DirectiveValidator
11+
{
12+
/**
13+
* Verify block content
14+
*
15+
* @param string $html
16+
* @return bool
17+
*/
18+
public function isValid(string $html): bool
19+
{
20+
if (preg_match('#<(pre|code)\b[^>]*>.*?{{\s*block\b.*?}}.*?</\1>#si', $html)) {
21+
return false;
22+
}
23+
24+
if (!preg_match_all('/{{\s*block\b(.*?)}}/si', $html, $matches, PREG_SET_ORDER)) {
25+
return true;
26+
}
27+
28+
foreach ($matches as $m) {
29+
$raw = $m[1] ?? '';
30+
31+
if ($raw !== strip_tags($raw)) {
32+
return false;
33+
}
34+
35+
// phpcs:ignore Magento2.Functions.DiscouragedFunction
36+
$body = html_entity_decode($raw, ENT_QUOTES);
37+
$params = $this->parseParams($body);
38+
39+
if (!isset($params['class']) && !isset($params['id'])) {
40+
return false;
41+
}
42+
43+
if (isset($params['class'])) {
44+
$class = trim((string)$params['class']);
45+
if (!preg_match('/^\\\\?[A-Za-z_][A-Za-z0-9_]*(\\\\[A-Za-z_][A-Za-z0-9_]*)*$/', $class)) {
46+
return false;
47+
}
48+
}
49+
50+
if (isset($params['name'])) {
51+
$name = (string)$params['name'];
52+
if (!preg_match('/^[A-Za-z0-9_.-]+$/', $name)) {
53+
return false;
54+
}
55+
}
56+
57+
if (isset($params['template'])) {
58+
$template = (string)$params['template'];
59+
if (!preg_match('/^[A-Za-z0-9_]+(?:_[A-Za-z0-9_]+)*::[A-Za-z0-9_.\-\/]+$/', $template)) {
60+
return false;
61+
}
62+
}
63+
64+
if (isset($params['id'])) {
65+
$id = (string)$params['id'];
66+
if (!preg_match('/^[A-Za-z0-9_\-]+$/', $id)) {
67+
return false;
68+
}
69+
}
70+
}
71+
72+
return true;
73+
}
74+
75+
/**
76+
* Parses a parameter string into an associative array.
77+
*
78+
* @param string $value
79+
* @return array
80+
*/
81+
private function parseParams(string $value): array
82+
{
83+
$params = [];
84+
if (preg_match_all('/(\w+)\s*=\s*("([^"]*)"|\'([^\']*)\'|(\S+))/u', $value, $mm, PREG_SET_ORDER)) {
85+
foreach ($mm as $p) {
86+
$params[$p[1]] = $p[3] ?? ($p[4] ?? $p[5] ?? '');
87+
}
88+
}
89+
return $params;
90+
}
91+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Cms\Plugin;
9+
10+
use Magento\Cms\Api\BlockRepositoryInterface;
11+
use Magento\Cms\Api\Data\BlockInterface;
12+
use Magento\Framework\Exception\LocalizedException;
13+
use Magento\Cms\Model\Validator\DirectiveValidator;
14+
15+
class BlockRepositoryValidatePlugin
16+
{
17+
/**
18+
* @var DirectiveValidator
19+
*/
20+
private DirectiveValidator $validator;
21+
22+
/**
23+
* @param DirectiveValidator $validator
24+
*/
25+
public function __construct(DirectiveValidator $validator)
26+
{
27+
$this->validator = $validator;
28+
}
29+
30+
/**
31+
* Validate Cms block before save
32+
*
33+
* @param BlockRepositoryInterface $subject
34+
* @param BlockInterface $block
35+
* @return BlockInterface[]
36+
* @throws LocalizedException
37+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
38+
*/
39+
public function beforeSave(BlockRepositoryInterface $subject, BlockInterface $block): array
40+
{
41+
if (!$this->validator->isValid((string)$block->getContent())) {
42+
throw new \Magento\Framework\Exception\LocalizedException(
43+
__('CMS block contains invalid content — please review and correct the block.')
44+
);
45+
}
46+
return [$block];
47+
}
48+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Cms\Plugin;
9+
10+
use Magento\Cms\Api\Data\PageInterface;
11+
use Magento\Cms\Api\PageRepositoryInterface;
12+
use Magento\Framework\Exception\LocalizedException;
13+
use Magento\Cms\Model\Validator\DirectiveValidator;
14+
15+
class PageRepositoryValidatePlugin
16+
{
17+
/**
18+
* @var DirectiveValidator
19+
*/
20+
private DirectiveValidator $validator;
21+
22+
/**
23+
* @param DirectiveValidator $validator
24+
*/
25+
public function __construct(DirectiveValidator $validator)
26+
{
27+
$this->validator = $validator;
28+
}
29+
30+
/**
31+
* Validate Cms Page before save
32+
*
33+
* @param PageRepositoryInterface $subject
34+
* @param PageInterface $page
35+
* @return PageInterface[]
36+
* @throws LocalizedException
37+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
38+
*/
39+
public function beforeSave(PageRepositoryInterface $subject, PageInterface $page): array
40+
{
41+
if (!$this->validator->isValid((string)$page->getContent())) {
42+
throw new \Magento\Framework\Exception\LocalizedException(
43+
__('CMS page contains invalid content — please review and correct the page.')
44+
);
45+
}
46+
return [$page];
47+
}
48+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Vendor\CmsValidate\Test\Unit\Model\Validator;
9+
10+
use Magento\Cms\Model\Validator\DirectiveValidator;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class DirectiveValidatorTest extends TestCase
14+
{
15+
private DirectiveValidator $validator;
16+
17+
protected function setUp(): void
18+
{
19+
$this->validator = new DirectiveValidator();
20+
}
21+
22+
public function testAllowsCleanBlockDirective(): void
23+
{
24+
$html = '{{block class="Magento\\Customer\\Block\\Form\\Register" name="home.form.customattributes" template="Magento_Customer::form/register.phtml"}}';
25+
$this->assertTrue($this->validator->isValid($html));
26+
}
27+
28+
public function testRejectsBlockInsidePre(): void
29+
{
30+
$html = '<pre class="code-java">{{block class="A\\B" id="x"}}</pre>';
31+
$this->assertFalse($this->validator->isValid($html));
32+
}
33+
34+
public function testRejectsHtmlInjectedParams(): void
35+
{
36+
$html = '{{block class=<span class="code-quote">"A\\B"</span> id="x"}}';
37+
$this->assertFalse($this->validator->isValid($html));
38+
}
39+
40+
public function testRequiresClassOrId(): void
41+
{
42+
$html = '{{block name="only-name"}}';
43+
$this->assertFalse($this->validator->isValid($html));
44+
}
45+
46+
public function testInvalidClassCharacters(): void
47+
{
48+
$html = '{{block class="Bad-Class" id="x"}}';
49+
$this->assertFalse($this->validator->isValid($html));
50+
}
51+
52+
public function testValidClassCharacters(): void
53+
{
54+
$html = '{{block class="Vendor\\Module\\Block\\My_Block" id="x"}}';
55+
$this->assertTrue($this->validator->isValid($html));
56+
}
57+
58+
public function testValidTemplateCharacters(): void
59+
{
60+
$html = '{{block class="A\\B" template="Magento_Customer::form/register.phtml"}}';
61+
$this->assertTrue($this->validator->isValid($html));
62+
}
63+
64+
public function testInvalidIdCharacters(): void
65+
{
66+
$html = '{{block id="bad id"}}';
67+
$this->assertFalse($this->validator->isValid($html));
68+
}
69+
70+
public function testValidIdCharacters(): void
71+
{
72+
$html = '{{block id="good_id-123"}}';
73+
$this->assertTrue($this->validator->isValid($html));
74+
}
75+
}

app/code/Magento/Cms/etc/adminhtml/di.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,10 @@
6868
<argument name="frontendUrlBuilder" xsi:type="object">Magento\Framework\Url</argument>
6969
</arguments>
7070
</type>
71+
<type name="Magento\Cms\Api\PageRepositoryInterface">
72+
<plugin name="vendor_cmsvalidate_page_save" type="Magento\Cms\Plugin\PageRepositoryValidatePlugin" sortOrder="10"/>
73+
</type>
74+
<type name="Magento\Cms\Api\BlockRepositoryInterface">
75+
<plugin name="vendor_cmsvalidate_block_save" type="Magento\Cms\Plugin\BlockRepositoryValidatePlugin" sortOrder="10"/>
76+
</type>
7177
</config>

0 commit comments

Comments
 (0)