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
@@ -145,6 +145,9 @@ final class AttrWriter
|
||||
{
|
||||
foreach ($normalized as $breakpoint => $states) {
|
||||
foreach ($states as $state => $value) {
|
||||
if ($breakpoint === 'desktop' && $state === 'value') {
|
||||
$this->cleaner->guardLoop($path, $value);
|
||||
}
|
||||
$normalized[$breakpoint][$state] = $this->cleaner->clean($module, $path, $value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,16 +61,43 @@ final class StoredNodeGuard
|
||||
/** @return list<string> German problems; [] when $root may move to the target post. */
|
||||
public function problems(Block $root): array
|
||||
{
|
||||
$problems = [];
|
||||
$this->loops($root, $problems);
|
||||
if ($this->sanitizer->allowsUnfilteredHtml()) {
|
||||
return [];
|
||||
return array_values(array_unique($problems));
|
||||
}
|
||||
|
||||
$problems = [];
|
||||
$this->walk($root, $problems);
|
||||
|
||||
return array_values(array_unique($problems));
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit L1: loop settings need list_users / read_private_posts also for accounts with unfiltered_html.
|
||||
*
|
||||
* @param list<string> $problems
|
||||
*/
|
||||
private function loops(Block $block, array &$problems): void
|
||||
{
|
||||
$attrs = $block->isDivi() ? $this->attributesOf($block) : null;
|
||||
$loop = is_array($attrs) ? AttrMap::get($attrs, 'module.advanced.loop') : null;
|
||||
$desktop = is_array($loop) && is_array($loop['desktop'] ?? null) ? $loop['desktop']['value'] ?? null : null;
|
||||
if ($desktop instanceof stdClass) {
|
||||
$desktop = get_object_vars($desktop);
|
||||
}
|
||||
try {
|
||||
$this->cleaner->guardLoop('module.advanced.loop', $desktop);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$problems[] = $e->getMessage();
|
||||
}
|
||||
|
||||
foreach ($block->parts() as $part) {
|
||||
if ($part instanceof Block) {
|
||||
$this->loops($part, $problems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param list<string> $problems */
|
||||
private function walk(Block $block, array &$problems): void
|
||||
{
|
||||
|
||||
@@ -167,6 +167,61 @@ final class ValueCleaner
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit L1: the loop settings of module.advanced.loop (desktop.value – all Divi reads) must not give the
|
||||
* account more than its rights: user loops list accounts and e-mails (list_users), and a post loop whose
|
||||
* subTypes are empty, "any", "attachment" or all unknown makes Divi query private posts too
|
||||
* (LoopUtils.php:1005-1037; read_private_posts). Fail closed: a loop that is not "on" is not checked.
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function guardLoop(string $path, mixed $desktopValue): void
|
||||
{
|
||||
if ($path !== 'module.advanced.loop' || ! is_array($desktopValue)) {
|
||||
return;
|
||||
}
|
||||
if (($desktopValue['enable'] ?? null) !== 'on') {
|
||||
return;
|
||||
}
|
||||
|
||||
$queryType = $desktopValue['queryType'] ?? 'post_types';
|
||||
$queryType = is_string($queryType) ? preg_replace('/[^a-z0-9_\-]/', '', strtolower($queryType)) : '';
|
||||
if (($queryType === 'user_roles' || $queryType === 'users') && ! $this->sanitizer->canListUsers()) {
|
||||
throw new InvalidArgumentException(
|
||||
'Eine Benutzer-Schleife listet Konten und E-Mail-Adressen und braucht das Recht list_users; '
|
||||
. 'ohne es nur im Divi-Builder einstellbar.'
|
||||
);
|
||||
}
|
||||
|
||||
$privatePosts = $queryType === 'post_types' && $this->listsPrivatePosts($desktopValue['subTypes'] ?? null);
|
||||
if ($privatePosts && ! $this->sanitizer->canReadPrivatePosts()) {
|
||||
throw new InvalidArgumentException(
|
||||
'Eine Beitrags-Schleife ohne Beitragstypen oder mit "any"/"attachment" zeigt auch private Beiträge '
|
||||
. 'und braucht das Recht read_private_posts; wähle konkrete Beitragstypen (subTypes).'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Divi's subTypes normalisation (LoopUtils.php:2869-2975), reduced: does the query reach private posts? */
|
||||
private function listsPrivatePosts(mixed $subTypes): bool
|
||||
{
|
||||
$raw = is_string($subTypes) ? explode(',', $subTypes) : (is_array($subTypes) ? $subTypes : []);
|
||||
$types = [];
|
||||
foreach ($raw as $entry) {
|
||||
$value = is_string($subTypes) ? $entry : (is_array($entry) ? ($entry['value'] ?? null) : null);
|
||||
if (is_string($value) && trim($value) !== '') {
|
||||
$types[] = preg_replace('/[^a-z0-9_\-]/', '', strtolower($value));
|
||||
}
|
||||
}
|
||||
|
||||
$existing = array_filter(
|
||||
$types,
|
||||
fn (string $type): bool => $type !== 'any' && $this->sanitizer->postTypeExists($type)
|
||||
);
|
||||
|
||||
return $existing === [] || in_array('any', $types, true) || in_array('attachment', $types, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps 2–8 for $value and everything inside it.
|
||||
*
|
||||
|
||||
@@ -32,6 +32,15 @@ interface ValueSanitizer
|
||||
*/
|
||||
public function allowsUnfilteredHtml(): bool;
|
||||
|
||||
/** Whether the current user has list_users (audit L1: user loops list accounts and e-mails). */
|
||||
public function canListUsers(): bool;
|
||||
|
||||
/** Whether the current user has read_private_posts (audit L1: post loops of any/attachment show private posts). */
|
||||
public function canReadPrivatePosts(): bool;
|
||||
|
||||
/** Whether post type $type exists (audit L1: Divi falls back to "any" when no listed type does). */
|
||||
public function postTypeExists(string $type): bool;
|
||||
|
||||
/**
|
||||
* Whether the current user may read post $id: select_post settings of dynamic content
|
||||
* references outside internal links (ruling S37); post_link_url_* use linkTargetType() (P10).
|
||||
|
||||
Reference in New Issue
Block a user