fix: Task 1 review round 3 - re-check $variable marker after sanitizing, meta-key rule

Critical-1/S28: sanitizers that strip <x>, NUL or %XX could rebuild the
$variable( marker after the raw check; every cleaned string (AttrWriter
at every depth, VariableRef settings) is checked again. S29: custom_meta_
map names and *meta_key setting values obey META_KEY. Minor-2: malformed
dynamic content maps fail with InvalidArgumentException.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 13:46:52 +02:00
co-authored by Claude Opus 5.5
parent eb99b360b4
commit 665e603abe
5 changed files with 215 additions and 14 deletions
@@ -172,6 +172,26 @@ final class AttrWriterTest extends TestCase
['desktop' => ['image' => ['url' => 'https://a.test/$variable({"type":"content","value":{"name":"post_title","settings":{}}})$']]],
self::TEXT,
];
// S28: markers a real sanitizer rebuilds after the raw check (verified in DDEV).
$reference = '({"type":"content","value":{"name":"post_title","settings":{"before":"<script>alert(1)</script>","after":""}}})$';
$cases['tag-split marker in rich text'] = [
'divi/text', 'content.innerContent', ['desktop' => '$variable<x>' . $reference], self::TEXT,
];
$cases['NUL-split marker in rich text'] = [
'divi/text', 'content.innerContent', ['desktop' => '$variable' . chr(0) . $reference], self::TEXT,
];
$cases['octet-split marker in a text title'] = [
'divi/heading', 'title.innerContent', ['desktop' => '$variable%41' . $reference], self::HEADING,
];
$cases['tag-split marker deep in a value'] = [
'divi/text', 'module.decoration.background', ['desktop' => ['color' => '$variable<x>' . $reference]], self::TEXT,
];
$cases['sanitizer-built valid reference'] = [
'divi/text',
'content.innerContent',
['desktop' => '$variable<x>({"type":"color","value":{"name":"gcid-a1b2","settings":{}}})$'],
self::TEXT,
];
$cases['javascript via a reference in a URL field'] = [
'divi/fullwidth-header',
'buttonOne.innerContent',
@@ -204,6 +224,21 @@ final class AttrWriterTest extends TestCase
self::assertSame($attrs, $result['attrs']);
}
/** S28: the split-marker rows carry no marker before sanitizing, but the sanitizer rebuilds one. */
public function testSplitMarkersAreRebuiltBySanitizing(): void
{
$sanitizer = ComposeFixture::sanitizer();
$rows = ['tag-split marker in rich text', 'NUL-split marker in rich text', 'octet-split marker in a text title',
'tag-split marker deep in a value'];
foreach (array_intersect_key(self::malicious(), array_flip($rows)) as $name => [, , $map]) {
$raw = (string) json_encode($map, JSON_UNESCAPED_SLASHES);
$value = is_array($map['desktop']) ? $map['desktop']['color'] : $map['desktop'];
self::assertStringNotContainsString('$variable(', $value, $name);
self::assertStringContainsString('$variable(', $sanitizer->text($value) . $sanitizer->html($value), $name . ' ' . $raw);
}
self::assertSame('$variable' . chr(0) . '(', substr(self::malicious()['NUL-split marker in rich text'][2]['desktop'], 0, 11));
}
/** Ruling S21: an editor's script in before/after is cleaned out, the reference re-serialized. */
public function testReferenceSettingsAreCleanedForEditors(): void
{