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'],