From c6daae131dc908a6b41523c2bf1a0388f1c9ed98 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Mon, 19 Oct 2026 14:41:06 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Variablenwerte=20f=C3=BCr=20Links/Bilder?= =?UTF-8?q?=20lehnen=20//host,=20/\host=20und=20Steuerzeichen=20ab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- src/Integrations/Divi/Design/VariableValue.php | 11 ++++++++--- .../Integrations/Divi/Design/VariableValueTest.php | 5 +++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Integrations/Divi/Design/VariableValue.php b/src/Integrations/Divi/Design/VariableValue.php index a9e5962..2ce8edb 100644 --- a/src/Integrations/Divi/Design/VariableValue.php +++ b/src/Integrations/Divi/Design/VariableValue.php @@ -27,7 +27,10 @@ final class VariableValue /** Control characters except the line feed. */ private const CONTROL = '/[\x00-\x09\x0B-\x1F\x7F]/'; - private const URL = '/^(?:https?:\/\/[^\s<>"]+|\/[^\s<>"]*|mailto:[^\s<>"]+|tel:[^\s<>"]+)$/iD'; + /** Every control character: Divi's esc_url_raw strips them, so the read-back would differ. */ + private const URL_CONTROL = '/[\x00-\x1F\x7F]/'; + + private const URL = '/^(?:https?:\/\/[^\s<>"]+|\/(?![\/\\\\])[^\s<>"]*|mailto:[^\s<>"]+|tel:[^\s<>"]+)$/iD'; private const WEB_URL = '/^https?:\/\/[^\s<>"]+$/iD'; @@ -74,7 +77,8 @@ final class VariableValue private static function links(string $value): ?string { - if (mb_strlen($value) > self::URL_MAX || preg_match(self::URL, $value) !== 1) { + if (mb_strlen($value) > self::URL_MAX || preg_match(self::URL, $value) !== 1 + || preg_match(self::URL_CONTROL, $value) === 1) { return 'Link: eine http(s)://- oder /-URL, mailto: oder tel: mit höchstens 2000 Zeichen.'; } @@ -84,7 +88,8 @@ final class VariableValue private static function images(string $value): ?string { $valid = preg_match('/^\d+$/D', $value) === 1 - || (mb_strlen($value) <= self::URL_MAX && preg_match(self::WEB_URL, $value) === 1); + || (mb_strlen($value) <= self::URL_MAX && preg_match(self::WEB_URL, $value) === 1 + && preg_match(self::URL_CONTROL, $value) !== 1); return $valid ? null : 'Bild: eine Anhang-ID (Ziffern) oder eine http(s)://-URL mit höchstens 2000 Zeichen.'; } diff --git a/tests/Unit/Integrations/Divi/Design/VariableValueTest.php b/tests/Unit/Integrations/Divi/Design/VariableValueTest.php index 8c2962a..fbd03f1 100644 --- a/tests/Unit/Integrations/Divi/Design/VariableValueTest.php +++ b/tests/Unit/Integrations/Divi/Design/VariableValueTest.php @@ -60,6 +60,11 @@ final class VariableValueTest extends TestCase 'links relative without slash' => ['links', 'kontakt/'], 'links with space' => ['links', 'https://example.com/a b'], 'links mailto empty' => ['links', 'mailto:'], + 'links protocol-relative' => ['links', '//evil.example/x'], + 'links backslash host' => ['links', '/\\evil.example/x'], + 'links control character' => ['links', "https://example.com/a\x01b"], + 'links delete character' => ['links', "/kontakt\x7F"], + 'images control character' => ['images', "https://example.com/a\x00.jpg"], 'links too long' => ['links', 'https://example.com/' . str_repeat('a', 2000)], 'images negative' => ['images', '-5'], 'images relative path' => ['images', '/wp-content/a.jpg'],