fix(audit L1): Schleifen in module.advanced.loop brauchen list_users (Benutzer) bzw. read_private_posts (any, attachment, leere subTypes), auch beim Kopieren
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
9c1d5fad22
commit
4a8a889f93
@@ -455,4 +455,33 @@ final class AttrWriterTest extends TestCase
|
||||
self::assertSame([], $adminRemoval['errors']);
|
||||
self::assertArrayNotHasKey('lottie', $adminRemoval['attrs']);
|
||||
}
|
||||
|
||||
/** Audit L1: the loop state of desktop.value is checked where attrs are written; other writes stay as they are. */
|
||||
public function testRefusesALoopThatNeedsRightsTheAccountLacks(): void
|
||||
{
|
||||
$writer = new AttrWriter(
|
||||
DiviFixtures::catalog(),
|
||||
ComposeFixture::placeholders(),
|
||||
new ValueCleaner(
|
||||
DiviFixtures::catalog(),
|
||||
ComposeFixture::references(),
|
||||
ComposeFixture::sanitizer(false, false, false)
|
||||
)
|
||||
);
|
||||
$loop = static fn (array $value): array => ['module.advanced.loop' => ['desktop' => $value]];
|
||||
|
||||
$users = $writer->apply('divi/text', self::TEXT, $loop(['enable' => 'on', 'queryType' => 'user_roles']), 'attrs');
|
||||
self::assertStringContainsString('list_users', $users['errors']['attrs.module.advanced.loop'] ?? '');
|
||||
self::assertArrayNotHasKey('module', $users['attrs']);
|
||||
|
||||
$posts = $writer->apply('divi/text', self::TEXT, $loop(['enable' => 'on']), 'attrs');
|
||||
self::assertStringContainsString('read_private_posts', $posts['errors']['attrs.module.advanced.loop'] ?? '');
|
||||
|
||||
$off = $writer->apply('divi/text', self::TEXT, $loop(['enable' => 'off']), 'attrs');
|
||||
self::assertSame([], $off['errors']);
|
||||
$safe = $writer->apply('divi/text', self::TEXT, $loop(
|
||||
['enable' => 'on', 'queryType' => 'post_types', 'subTypes' => [['value' => 'post']]]
|
||||
), 'attrs');
|
||||
self::assertSame([], $safe['errors']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +27,16 @@ final class StoredNodeGuardTest extends TestCase
|
||||
private const PROBE_HTML = '<iframe data-probe="1"></iframe>';
|
||||
private const PROBE_SCRIPT = '<script data-probe="1">/* probe */</script>';
|
||||
|
||||
private static function guard(bool $unfiltered = false): StoredNodeGuard
|
||||
{
|
||||
private static function guard(
|
||||
bool $unfiltered = false,
|
||||
bool $listUsers = true,
|
||||
bool $readPrivate = true
|
||||
): StoredNodeGuard {
|
||||
$references = new VariableRef(
|
||||
DiviFixtures::designSystem(),
|
||||
DynamicContentMap::fromOptions(DiviFixtures::dynamicContentRegistry())
|
||||
);
|
||||
$sanitizer = ComposeFixture::sanitizer($unfiltered);
|
||||
$sanitizer = ComposeFixture::sanitizer($unfiltered, $listUsers, $readPrivate);
|
||||
|
||||
$cleaner = new ValueCleaner(DiviFixtures::catalog(), $references, $sanitizer);
|
||||
|
||||
@@ -411,6 +414,21 @@ final class StoredNodeGuardTest extends TestCase
|
||||
return false;
|
||||
}
|
||||
|
||||
public function canListUsers(): bool
|
||||
{
|
||||
return $this->inner->canListUsers();
|
||||
}
|
||||
|
||||
public function canReadPrivatePosts(): bool
|
||||
{
|
||||
return $this->inner->canReadPrivatePosts();
|
||||
}
|
||||
|
||||
public function postTypeExists(string $type): bool
|
||||
{
|
||||
return $this->inner->postTypeExists($type);
|
||||
}
|
||||
|
||||
public function canReadPost(int $id): bool
|
||||
{
|
||||
return $this->inner->canReadPost($id);
|
||||
@@ -472,4 +490,28 @@ final class StoredNodeGuardTest extends TestCase
|
||||
self::assertSame([], self::wpLikeGuard()->problems(self::link('//evil.example')));
|
||||
self::assertNotSame([], self::wpLikeGuard(false)->problems(self::link('//evil.example')));
|
||||
}
|
||||
|
||||
/** Audit L1: copied loops need the same rights as written ones – also for accounts with unfiltered_html. */
|
||||
public function testACopiedLoopNeedsTheRightsItsSettingsNeed(): void
|
||||
{
|
||||
$user = self::block(self::node('divi/text', ['module' => ['advanced' => ['loop' => self::desktop(
|
||||
['enable' => 'on', 'queryType' => 'user_roles']
|
||||
)]]]));
|
||||
$private = self::block(self::node('divi/text', ['module' => ['advanced' => ['loop' => self::desktop(
|
||||
['enable' => 'on', 'queryType' => 'post_types', 'subTypes' => []]
|
||||
)]]]));
|
||||
|
||||
foreach ([false, true] as $unfiltered) {
|
||||
self::assertStringContainsString(
|
||||
'list_users',
|
||||
implode(' ', self::guard($unfiltered, false, true)->problems($user))
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
'read_private_posts',
|
||||
implode(' ', self::guard($unfiltered, true, false)->problems($private))
|
||||
);
|
||||
}
|
||||
self::assertSame([], self::guard(true, true, true)->problems($user));
|
||||
self::assertSame([], self::guard(true, true, true)->problems($private));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use LIT\MCP\Integrations\Divi\Compose\ValueCleaner;
|
||||
use LIT\MCP\Integrations\Divi\Compose\ValueKind;
|
||||
use LIT\MCP\Tests\Support\ComposeFixture;
|
||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||
@@ -13,6 +14,87 @@ use PHPUnit\Framework\TestCase;
|
||||
/** The choke point (ValueCleaner): tests moved from AttrWriterTest with clean()/kindOf() (H1). */
|
||||
final class ValueCleanerTest extends TestCase
|
||||
{
|
||||
private static function loopCleaner(bool $listUsers, bool $readPrivate): ValueCleaner
|
||||
{
|
||||
return new ValueCleaner(
|
||||
DiviFixtures::catalog(),
|
||||
ComposeFixture::references(),
|
||||
ComposeFixture::sanitizer(false, $listUsers, $readPrivate)
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string, array{array<string, mixed>}> Loops that read private posts or user data (audit L1) */
|
||||
public static function riskyLoops(): array
|
||||
{
|
||||
$types = static fn (string ...$values): array => array_map(
|
||||
static fn (string $value): array => ['value' => $value, 'label' => $value],
|
||||
$values
|
||||
);
|
||||
$post = ['enable' => 'on', 'queryType' => 'post_types'];
|
||||
|
||||
return [
|
||||
'no subTypes' => [$post],
|
||||
'empty subTypes' => [$post + ['subTypes' => []]],
|
||||
'queryType missing, empty subTypes' => [['enable' => 'on', 'subTypes' => []]],
|
||||
'any' => [$post + ['subTypes' => $types('any')]],
|
||||
'attachment among others' => [$post + ['subTypes' => $types('post', 'attachment')]],
|
||||
'only unknown types (Divi falls back to any)' => [$post + ['subTypes' => $types('gibtesnicht')]],
|
||||
'comma string with any' => [$post + ['subTypes' => 'post, ANY']],
|
||||
'entries without value' => [$post + ['subTypes' => [['label' => 'x']]]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider riskyLoops
|
||||
* @param array<string, mixed> $loop
|
||||
*/
|
||||
public function testPostLoopsThatMayShowPrivatePostsNeedReadPrivatePosts(array $loop): void
|
||||
{
|
||||
try {
|
||||
self::loopCleaner(true, false)->guardLoop('module.advanced.loop', $loop);
|
||||
self::fail('expected a refusal without read_private_posts');
|
||||
} catch (InvalidArgumentException $e) {
|
||||
self::assertStringContainsString('read_private_posts', $e->getMessage());
|
||||
}
|
||||
|
||||
self::loopCleaner(false, true)->guardLoop('module.advanced.loop', $loop);
|
||||
$this->addToAssertionCount(1);
|
||||
}
|
||||
|
||||
public function testUserLoopsNeedListUsersAlsoUnderDiviAliasAndSanitizeKey(): void
|
||||
{
|
||||
foreach (['user_roles', 'users', ' User_Roles '] as $queryType) {
|
||||
$loop = ['enable' => 'on', 'queryType' => $queryType];
|
||||
try {
|
||||
self::loopCleaner(false, true)->guardLoop('module.advanced.loop', $loop);
|
||||
self::fail('expected a refusal without list_users: ' . $queryType);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
self::assertStringContainsString('list_users', $e->getMessage());
|
||||
}
|
||||
self::loopCleaner(true, false)->guardLoop('module.advanced.loop', $loop);
|
||||
}
|
||||
$this->addToAssertionCount(3);
|
||||
}
|
||||
|
||||
public function testSafeLoopsPassWithoutTheExtraRights(): void
|
||||
{
|
||||
$cleaner = self::loopCleaner(false, false);
|
||||
$on = ['enable' => 'on'];
|
||||
foreach ([
|
||||
$on + ['queryType' => 'post_types', 'subTypes' => [['value' => 'post'], ['value' => 'page']]],
|
||||
$on + ['queryType' => 'post_types', 'subTypes' => 'post'],
|
||||
$on + ['queryType' => 'post_taxonomies', 'subTypes' => []],
|
||||
$on + ['queryType' => 'menus'],
|
||||
$on + ['queryType' => 'current_page'],
|
||||
['enable' => 'off', 'queryType' => 'user_roles'],
|
||||
['queryType' => 'post_types'],
|
||||
] as $loop) {
|
||||
$cleaner->guardLoop('module.advanced.loop', $loop);
|
||||
}
|
||||
$cleaner->guardLoop('module.advanced.link', 'irrelevant');
|
||||
$this->addToAssertionCount(1);
|
||||
}
|
||||
|
||||
/** Ruling S3/S3a: kind from the catalog, per sub-field for composite paths, else key name, else text. */
|
||||
public function testRoutesByCatalogKindAndKeyNameAtEveryDepth(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user