fix: Task 3 review round 1 - strip control chars before the sanitizer (S32a), S34 minors
Control characters now go on the raw value in AttrWriter::cleanNode and
VariableRef::setting; after the sanitizer only rejection remains ('<' + VT/FF
no longer turns into a tag). VALUE_KEY anchored with D, exact column-width
fractions, nested rows in columns, specialty columns refused, fullwidth by
catalog category, preset ambiguity in DesignSystem::modulePresetId().
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -177,9 +177,6 @@ final class AttrWriterTest extends TestCase
|
||||
$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,
|
||||
];
|
||||
@@ -228,7 +225,7 @@ final class AttrWriterTest extends TestCase
|
||||
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',
|
||||
$rows = ['tag-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);
|
||||
@@ -236,7 +233,6 @@ final class AttrWriterTest extends TestCase
|
||||
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. */
|
||||
@@ -385,7 +381,10 @@ final class AttrWriterTest extends TestCase
|
||||
self::assertSame("<p>a\nb\r</p>", $writer->cleanValue('divi/text', 'content.innerContent', "<p>a\x0b\nb\r\x08</p>"));
|
||||
self::assertSame('Titel', $writer->cleanValue('divi/heading', 'title.innerContent', "Ti\x00tel\x7f"));
|
||||
|
||||
$tail = '({"type":"color","value":{"name":"gcid-a1b2","settings":{}}})$';
|
||||
// S32a: controls go on the RAW value, so a control-split marker IS a marker: the value goes
|
||||
// through canonical() like any reference – validated, cleaned (S21) and re-serialized.
|
||||
$tail = '({"type":"content","value":{"name":"post_title","settings":{"before":"<script>x()</script>","after":""}}})$';
|
||||
$canonical = '$variable({"type":"content","value":{"name":"post_title","settings":{"before":"","after":""}}})$';
|
||||
$targets = [
|
||||
'text' => ['divi/heading', 'title.innerContent'],
|
||||
'html' => ['divi/text', 'content.innerContent'],
|
||||
@@ -393,19 +392,55 @@ final class AttrWriterTest extends TestCase
|
||||
];
|
||||
foreach ([chr(0), chr(1), chr(8), chr(27), chr(127)] as $control) {
|
||||
$value = '$variable' . $control . $tail;
|
||||
// The fake keeps them like sanitize_text_field does: the production code must catch it.
|
||||
self::assertStringNotContainsString('$variable(', ComposeFixture::sanitizer()->text($value));
|
||||
// The fake keeps them like sanitize_text_field does: the production code handles it.
|
||||
self::assertStringNotContainsString('$variable(', ComposeFixture::sanitizer()->text('$variable' . $control . '(x'));
|
||||
foreach ($targets as $kind => [$module, $path]) {
|
||||
try {
|
||||
$writer->cleanValue($module, $path, $value);
|
||||
self::fail(sprintf('%s: control %d not caught', $kind, ord($control)));
|
||||
} catch (InvalidArgumentException $e) {
|
||||
self::assertStringContainsString('allein im Wert', $e->getMessage());
|
||||
}
|
||||
self::assertSame($canonical, $writer->cleanValue($module, $path, $value), $kind . ' ' . ord($control));
|
||||
}
|
||||
// Embedded in text it is an embedded reference: refused.
|
||||
try {
|
||||
$writer->cleanValue('divi/heading', 'title.innerContent', 'Hallo $variable' . $control . $tail);
|
||||
self::fail('embedded control-split reference accepted');
|
||||
} catch (InvalidArgumentException $e) {
|
||||
self::assertStringContainsString('allein im Wert', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling S32a: "<" followed by VT/FF is a literal "<" for strip_tags / sanitize_text_field;
|
||||
* stripping the control character only after the sanitizer would build a real tag. Controls
|
||||
* go on the RAW value, so the sanitizer sees – and removes – the tag.
|
||||
*/
|
||||
public function testControlCharactersGoBeforeTheSanitizer(): void
|
||||
{
|
||||
$writer = ComposeFixture::attrWriter();
|
||||
$img = '<' . chr(11) . 'img src=x onerror=alert(1)>';
|
||||
$script = '<' . chr(12) . 'script>alert(1)</script>';
|
||||
// The fake reproduces the WordPress behaviour the attack relies on.
|
||||
self::assertStringContainsString('<' . chr(11) . 'img', ComposeFixture::sanitizer()->text($img));
|
||||
|
||||
foreach ([$img . 'Titel', $script . 'Titel'] as $value) {
|
||||
$result = $writer->apply('divi/heading', self::HEADING, ['title.innerContent' => ['desktop' => $value]], 'attrs');
|
||||
self::assertSame([], $result['errors']);
|
||||
$stored = $result['attrs']['title']['innerContent']['desktop']['value'];
|
||||
self::assertStringNotContainsString('<', $stored);
|
||||
|
||||
$button = $writer->cleanValue('divi/button', 'button.innerContent', $value, 'text');
|
||||
self::assertStringNotContainsString('<', $button);
|
||||
}
|
||||
}
|
||||
|
||||
/** Ruling S34: value keys are whole identifiers – "$" must not match before a trailing newline. */
|
||||
public function testValueKeysRejectATrailingNewline(): void
|
||||
{
|
||||
$result = ComposeFixture::attrWriter()->apply('divi/text', self::TEXT, [
|
||||
'module.decoration.spacing' => ['desktop' => ['padding' => ["top\n" => '1px']]],
|
||||
], 'attrs');
|
||||
|
||||
self::assertStringContainsString('Schlüssel', $result['errors']['attrs.module.decoration.spacing']);
|
||||
}
|
||||
|
||||
/** Ruling S33a: a kind override may only tighten the routed kind (code < html < text; url is its own family). */
|
||||
public function testKindOverrideMayOnlyTighten(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user