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:
@@ -150,6 +150,13 @@ final class VariableRefTest extends TestCase
|
||||
'extra value key' => ['$variable({"type":"color","value":{"name":"gcid-a1b2","settings":{},"x":1}})$', 'x'],
|
||||
'greedy smuggling' => ['$variable({"a":"})$<script>alert(1)</script>"})$', 'allein im Wert'],
|
||||
'nested reference in before' => [self::content('post_title', '{"before":"$variable(x)$","after":""}'), 'allein im Wert'],
|
||||
// S28: a marker the sanitizer rebuilds inside a cleaned setting (html(), text()).
|
||||
'tag-split marker in before' => [self::content('post_title', '{"before":"$variable<x>(y","after":""}'), 'allein im Wert'],
|
||||
'NUL-split marker in after' => [
|
||||
self::content('post_title', '{"before":"","after":"$variable' . self::escape('0000') . '(y"}'),
|
||||
'allein im Wert',
|
||||
],
|
||||
'octet-split marker in date format' => [self::content('post_date', '{"custom_date_format":"$variable%41(y"}'), 'allein im Wert'],
|
||||
'nested marker in before' => [self::content('post_title', '{"before":"$variable(","after":""}'), 'allein im Wert'],
|
||||
'broken json' => ['$variable(kaputt)$', 'JSON'],
|
||||
'trailing text' => [self::content('post_title', '{}') . ' ', 'allein im Wert'],
|
||||
@@ -242,6 +249,77 @@ final class VariableRefTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
/** S28: the split-marker rows really carry no marker before sanitizing. */
|
||||
public function testSplitMarkerRowsHaveNoRawMarker(): void
|
||||
{
|
||||
$rows = self::malicious();
|
||||
foreach (['tag-split marker in before', 'NUL-split marker in after', 'octet-split marker in date format'] as $row) {
|
||||
$payload = substr($rows[$row][0], strlen('$variable('));
|
||||
self::assertStringNotContainsString('$variable(', $payload, $row);
|
||||
}
|
||||
self::assertStringContainsString(chr(92) . 'u0000(y', $rows['NUL-split marker in after'][0]);
|
||||
}
|
||||
|
||||
/** Ruling S29: custom_meta_ names and meta-key settings obey META_KEY, map or not. */
|
||||
public function testMetaKeysObeyTheRuleEvenFromTheMap(): void
|
||||
{
|
||||
$ref = self::ref([
|
||||
'post_meta_key' => ['before' => 'text', 'meta_key' => 'text', 'select_meta_key' => 'select'],
|
||||
'custom_meta__edit_lock' => ['before' => 'text'],
|
||||
'loop_post_meta_key' => ['loop_meta_key' => 'text'],
|
||||
]);
|
||||
$sanitizer = ComposeFixture::sanitizer();
|
||||
|
||||
self::assertSame(
|
||||
self::content('post_meta_key', '{"meta_key":"telefon","select_meta_key":"custom_meta_fax"}'),
|
||||
$ref->canonical(self::content('post_meta_key', '{"meta_key":"telefon","select_meta_key":"custom_meta_fax"}'), $sanitizer)
|
||||
);
|
||||
self::assertSame(
|
||||
self::content('post_meta_key', '{"select_meta_key":""}'),
|
||||
$ref->canonical(self::content('post_meta_key', '{"select_meta_key":""}'), $sanitizer)
|
||||
);
|
||||
|
||||
$rejected = [
|
||||
self::content('post_meta_key', '{"meta_key":"_wp_page_template"}') => 'meta_key',
|
||||
self::content('post_meta_key', '{"select_meta_key":"custom_meta__edit_lock"}') => 'select_meta_key',
|
||||
self::content('post_meta_key', '{"meta_key":"a b"}') => 'meta_key',
|
||||
self::content('loop_post_meta_key', '{"loop_meta_key":"_secret"}') => 'loop_meta_key',
|
||||
self::content('custom_meta__edit_lock', '{}') => 'custom_meta__edit_lock',
|
||||
];
|
||||
foreach ($rejected as $value => $fragment) {
|
||||
try {
|
||||
$ref->canonical($value, $sanitizer);
|
||||
self::fail('Accepted: ' . $value);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
self::assertStringContainsString($fragment, $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string, array{array<array-key, mixed>}> */
|
||||
public static function malformedMaps(): array
|
||||
{
|
||||
return [
|
||||
'setting type is an array' => [['post_title' => ['before' => ['type' => 'text']]]],
|
||||
'settings not an array' => [['post_title' => 'text']],
|
||||
'numeric setting key' => [['post_title' => ['text']]],
|
||||
'numeric name' => [[['before' => 'text']]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Minor-2: a malformed map fails early and clearly.
|
||||
*
|
||||
* @dataProvider malformedMaps
|
||||
* @param array<array-key, mixed> $map
|
||||
*/
|
||||
public function testRejectsMalformedMaps(array $map): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
|
||||
self::ref($map); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
public function testDetectsReferences(): void
|
||||
{
|
||||
self::assertTrue(VariableRef::contains('a $variable( b'));
|
||||
|
||||
Reference in New Issue
Block a user