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
|
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
|
von Modulen schreibt LIT MCP nur für Benutzer mit unfiltered_html. Divis eigene Speicherfilter bleiben
|
||||||
aktiv.
|
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
|
* 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
|
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
|
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.';
|
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. */
|
/** wp_insert_post() ignores an empty page_template; this value selects the default template. */
|
||||||
public const DEFAULT_TEMPLATE = 'default';
|
public const DEFAULT_TEMPLATE = 'default';
|
||||||
|
|
||||||
@@ -55,7 +66,11 @@ class PostWriter
|
|||||||
public static function inputProperties(): array
|
public static function inputProperties(): array
|
||||||
{
|
{
|
||||||
return [
|
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'],
|
'excerpt' => ['type' => 'string'],
|
||||||
'slug' => ['type' => 'string'],
|
'slug' => ['type' => 'string'],
|
||||||
'parent' => ['type' => 'integer', 'minimum' => 0],
|
'parent' => ['type' => 'integer', 'minimum' => 0],
|
||||||
@@ -86,6 +101,9 @@ class PostWriter
|
|||||||
public function prepare(string $postType, array $input, bool $isCreate): array|WP_Error
|
public function prepare(string $postType, array $input, bool $isCreate): array|WP_Error
|
||||||
{
|
{
|
||||||
$errors = [];
|
$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(
|
$fields = $this->validator->validate(
|
||||||
$this->schema->fields($postType),
|
$this->schema->fields($postType),
|
||||||
@@ -114,6 +132,12 @@ class PostWriter
|
|||||||
];
|
];
|
||||||
$errors = array_merge($errors, array_filter($checks, static fn (?string $error): bool => $error !== null));
|
$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 !== []) {
|
if ($errors !== []) {
|
||||||
return Errors::invalidInput('Eingaben sind ungültig; nichts wurde gespeichert.', $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];
|
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
|
public function testParentMustBeAReadableNonDescendantEntryOfAHierarchicalType(): void
|
||||||
{
|
{
|
||||||
$writer = $this->writerWithoutFields();
|
$writer = $this->writerWithoutFields();
|
||||||
|
|||||||
Reference in New Issue
Block a user