fix(security): raw content refuses Divi markup (audit M1)
lit/content-create and lit/content-update refuse content containing the divi/* block delimiters WordPress' block parser accepts or Divi 4 shortcodes, so the builder-only gates (S7/S26) can't be bypassed. Other block markup stays allowed (normal kses). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,10 @@ Einstellungen: Einstellungen → LIT MCP (Verbindung, Zugriff, Tools, Protokoll,
|
||||
Inhalte des Code-Moduls bleiben nur mit unfiltered_html ungefiltert; eigenes HTML, CSS und HTML-Attribute
|
||||
von Modulen schreibt LIT MCP nur für Benutzer mit unfiltered_html. Divis eigene Speicherfilter bleiben
|
||||
aktiv.
|
||||
* Das Feld content von lit-content-create und lit-content-update nimmt kein Divi-Markup an (Divi-Blöcke
|
||||
<!-- wp:divi/… und Divi-4-Shortcodes [et_pb_…]); Divi-Inhalte entstehen nur über divi bzw. die
|
||||
lit-divi-*-Tools und damit über dieselbe Bereinigung. Anderes Block-Markup bleibt erlaubt und läuft wie
|
||||
im WordPress-Editor durch die kses-Filterung von WordPress.
|
||||
* Unberührte Teile einer Divi-Seite bleiben beim Speichern byte-identisch. Ausnahme: Divi selbst normalisiert
|
||||
Seiten mit eigenen HTML-/Attribut-Feldern beim Speichern, sofern sie noch nicht in Divis eigener
|
||||
gespeicherter Form vorliegen (z. B. importierter Inhalt); über den Divi-Builder oder LIT MCP gespeicherte
|
||||
|
||||
@@ -38,6 +38,17 @@ class PostWriter
|
||||
|
||||
private const META_DENIED = 'Keine Berechtigung, dieses Feld zu ändern.';
|
||||
|
||||
/**
|
||||
* Divi markup in raw content (security M1): the block delimiters WordPress' block parser accepts
|
||||
* for divi/* blocks (Divi's BlockParser extends it: "<!--", whitespace, optional "/", lower-case
|
||||
* "wp:divi/") and Divi 4 shortcodes. Such content would bypass the builder-only gates of the Divi
|
||||
* tools (rulings S7/S26); other block markup stays allowed and goes through WordPress' kses.
|
||||
*/
|
||||
private const DIVI_MARKUP = '~<!--\s+/?wp:divi/|\[/?et_pb_~';
|
||||
|
||||
private const DIVI_MARKUP_HINT = 'Divi-Inhalte über `divi` (lit-content-create) bzw. die lit-divi-*-Tools '
|
||||
. '(lit-divi-insert, lit-divi-update, lit-divi-move, lit-divi-delete) ändern.';
|
||||
|
||||
/** wp_insert_post() ignores an empty page_template; this value selects the default template. */
|
||||
public const DEFAULT_TEMPLATE = 'default';
|
||||
|
||||
@@ -55,7 +66,11 @@ class PostWriter
|
||||
public static function inputProperties(): array
|
||||
{
|
||||
return [
|
||||
'content' => ['type' => 'string', 'description' => 'HTML bzw. Block-Markup (nicht für Divi-Inhalte).'],
|
||||
'content' => [
|
||||
'type' => 'string',
|
||||
'description' => 'HTML bzw. Block-Markup; Divi-Markup wird abgelehnt (Divi-Inhalte über divi '
|
||||
. 'bei lit-content-create bzw. die lit-divi-* Tools).',
|
||||
],
|
||||
'excerpt' => ['type' => 'string'],
|
||||
'slug' => ['type' => 'string'],
|
||||
'parent' => ['type' => 'integer', 'minimum' => 0],
|
||||
@@ -86,6 +101,9 @@ class PostWriter
|
||||
public function prepare(string $postType, array $input, bool $isCreate): array|WP_Error
|
||||
{
|
||||
$errors = [];
|
||||
if (is_string($input['content'] ?? null) && preg_match(self::DIVI_MARKUP, $input['content']) === 1) {
|
||||
$errors['content'] = 'Divi-Markup ist in content nicht erlaubt.';
|
||||
}
|
||||
|
||||
$fields = $this->validator->validate(
|
||||
$this->schema->fields($postType),
|
||||
@@ -114,6 +132,12 @@ class PostWriter
|
||||
];
|
||||
$errors = array_merge($errors, array_filter($checks, static fn (?string $error): bool => $error !== null));
|
||||
|
||||
if (isset($errors['content'])) {
|
||||
$message = 'Eingaben sind ungültig; nichts wurde gespeichert.';
|
||||
|
||||
return Errors::invalidInput($message, $errors, self::DIVI_MARKUP_HINT);
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
return Errors::invalidInput('Eingaben sind ungültig; nichts wurde gespeichert.', $errors);
|
||||
}
|
||||
|
||||
@@ -75,6 +75,42 @@ final class PostWriterTest extends TestCase
|
||||
return (string) self::errorFields($result)[$path];
|
||||
}
|
||||
|
||||
/**
|
||||
* Security M1: raw content must not introduce Divi layouts (the builder-only gates S7/S26 of
|
||||
* the Divi tools would not apply). Refused are exactly the delimiters WordPress' block parser
|
||||
* (and Divi's BlockParser, which extends it) accepts – "<!--" + whitespace + optional "/" +
|
||||
* lower-case "wp:divi/" – plus Divi 4 shortcodes; other block markup stays allowed (kses).
|
||||
*/
|
||||
public function testContentWithDiviMarkupIsRefusedOnCreateAndUpdate(): void
|
||||
{
|
||||
$writer = $this->writerWithoutFields();
|
||||
$refused = [
|
||||
'<!-- wp:divi/section --><!-- /wp:divi/section -->',
|
||||
"<p>x</p><!--\twp:divi/text {\"a\":1} /-->",
|
||||
"<!--\n\n/wp:divi/row -->",
|
||||
'[et_pb_section][/et_pb_section]',
|
||||
];
|
||||
foreach ($refused as $content) {
|
||||
foreach ([true, false] as $isCreate) {
|
||||
$result = $writer->prepare('page', ['content' => $content], $isCreate);
|
||||
$payload = self::assertToolError($result, 'validation_failed', $content);
|
||||
self::assertStringContainsString('Divi', self::errorFields($result)['content']);
|
||||
self::assertStringContainsString('`divi`', (string) $payload['hint']);
|
||||
self::assertStringContainsString('lit-divi-', (string) $payload['hint']);
|
||||
}
|
||||
}
|
||||
|
||||
$allowed = [
|
||||
'<!-- wp:paragraph --><p>Divi ist toll</p><!-- /wp:paragraph -->',
|
||||
'<!--wp:divi/text /-->',
|
||||
'<!-- WP:DIVI/text /-->',
|
||||
'<p>wp:divi/ im Text</p>',
|
||||
];
|
||||
foreach ($allowed as $content) {
|
||||
self::assertIsArray($writer->prepare('page', ['content' => $content], true), $content);
|
||||
}
|
||||
}
|
||||
|
||||
public function testParentMustBeAReadableNonDescendantEntryOfAHierarchicalType(): void
|
||||
{
|
||||
$writer = $this->writerWithoutFields();
|
||||
|
||||
Reference in New Issue
Block a user