From e0cda493c1ecb58b1d18d9cb95f1f946993bb3cb Mon Sep 17 00:00:00 2001 From: Danny van der Sluijs Date: Fri, 21 Aug 2026 11:25:56 +0200 Subject: [PATCH 1/4] fix: Handle null byte when validating date/time formats --- .../Drafts/Draft06/FormatConstraint.php | 6 +++- .../Drafts/Draft07/FormatConstraint.php | 7 +++- .../Drafts/Draft2019/FormatConstraint.php | 7 +++- .../Constraints/FormatConstraint.php | 9 +++-- .../Draft06/FormatConstraintTest.php | 34 +++++++++++++++++++ .../Draft07/FormatConstraintTest.php | 34 +++++++++++++++++++ .../Draft2019/FormatConstraintTest.php | 34 +++++++++++++++++++ tests/Constraints/FormatTest.php | 2 ++ 8 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 tests/Constraints/Draft06/FormatConstraintTest.php create mode 100644 tests/Constraints/Draft07/FormatConstraintTest.php create mode 100644 tests/Constraints/Draft2019/FormatConstraintTest.php diff --git a/src/JsonSchema/Constraints/Drafts/Draft06/FormatConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft06/FormatConstraint.php index ec61b73b..d8cc0e00 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft06/FormatConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft06/FormatConstraint.php @@ -125,7 +125,11 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n private function validateDateTime(string $datetime, string $format): bool { - $dt = \DateTime::createFromFormat($format, $datetime); + try { + $dt = \DateTime::createFromFormat($format, $datetime); + } catch (\Throwable $e) { + return false; + } if (!$dt) { return false; diff --git a/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php index 8ddc879d..fd5474b6 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php @@ -149,7 +149,12 @@ private function validateDateTime(string $datetime, string $format): bool $input = sprintf('%s59%s', substr($datetime, 0, 6), substr($datetime, 8)); } - $dt = \DateTimeImmutable::createFromFormat($format, $input); + try { + $dt = \DateTimeImmutable::createFromFormat($format, $input); + } catch (\Throwable $e) { + return false; + } + if (!$dt) { return false; } diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/FormatConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft2019/FormatConstraint.php index 5fb47c87..419f3cb2 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft2019/FormatConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/FormatConstraint.php @@ -140,7 +140,12 @@ private function validateDateTime(string $datetime, string $format): bool $input = sprintf('%s59%s', substr($datetime, 0, 6), substr($datetime, 8)); } - $dt = \DateTimeImmutable::createFromFormat($format, $input); + try { + $dt = \DateTimeImmutable::createFromFormat($format, $input); + } catch (\Throwable $e) { + return false; + } + if (!$dt) { return false; } diff --git a/src/JsonSchema/Constraints/FormatConstraint.php b/src/JsonSchema/Constraints/FormatConstraint.php index c38488dd..64d020d4 100644 --- a/src/JsonSchema/Constraints/FormatConstraint.php +++ b/src/JsonSchema/Constraints/FormatConstraint.php @@ -16,6 +16,7 @@ use JsonSchema\Rfc3339; use JsonSchema\Tool\Validator\RelativeReferenceValidator; use JsonSchema\Tool\Validator\UriValidator; +use Throwable; /** * Validates against the "format" property @@ -152,9 +153,13 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = } } - protected function validateDateTime($datetime, $format) + protected function validateDateTime($datetime, $format): bool { - $dt = \DateTime::createFromFormat($format, (string) $datetime); + try { + $dt = \DateTime::createFromFormat($format, (string) $datetime); + } catch (Throwable $e) { + return false; + } if (!$dt) { return false; diff --git a/tests/Constraints/Draft06/FormatConstraintTest.php b/tests/Constraints/Draft06/FormatConstraintTest.php new file mode 100644 index 00000000..33992533 --- /dev/null +++ b/tests/Constraints/Draft06/FormatConstraintTest.php @@ -0,0 +1,34 @@ +format = $format; + + $validator->check($string, $schema); + $this->assertCount(1, $validator->getErrors(), 'Expected 1 error'); + } + + public function getInvalidFormats(): Generator + { + yield 'Date-time format with value containing null byte' => ['2020-01-01T12:34:56\x00', 'date']; + + yield 'Date format with value containing null byte' => ['2020-01-01\x00', 'date']; + + yield 'Time format with value containing null byte' => ['13:37:00\x00', 'time']; + } +} diff --git a/tests/Constraints/Draft07/FormatConstraintTest.php b/tests/Constraints/Draft07/FormatConstraintTest.php new file mode 100644 index 00000000..a5ecd44e --- /dev/null +++ b/tests/Constraints/Draft07/FormatConstraintTest.php @@ -0,0 +1,34 @@ +format = $format; + + $validator->check($string, $schema); + $this->assertCount(1, $validator->getErrors(), 'Expected 1 error'); + } + + public function getInvalidFormats(): Generator + { + yield 'Date-time format with value containing null byte' => ['2020-01-01T12:34:56\x00', 'date']; + + yield 'Date format with value containing null byte' => ['2020-01-01\x00', 'date']; + + yield 'Time format with value containing null byte' => ['13:37:00\x00', 'time']; + } +} diff --git a/tests/Constraints/Draft2019/FormatConstraintTest.php b/tests/Constraints/Draft2019/FormatConstraintTest.php new file mode 100644 index 00000000..4c3420ad --- /dev/null +++ b/tests/Constraints/Draft2019/FormatConstraintTest.php @@ -0,0 +1,34 @@ +format = $format; + + $validator->check($string, $schema); + $this->assertCount(1, $validator->getErrors(), 'Expected 1 error'); + } + + public function getInvalidFormats(): Generator + { + yield 'Date-time format with value containing null byte' => ['2020-01-01T12:34:56\x00', 'date']; + + yield 'Date format with value containing null byte' => ['2020-01-01\x00', 'date']; + + yield 'Time format with value containing null byte' => ['13:37:00\x00', 'time']; + } +} diff --git a/tests/Constraints/FormatTest.php b/tests/Constraints/FormatTest.php index 72471054..de847a5e 100644 --- a/tests/Constraints/FormatTest.php +++ b/tests/Constraints/FormatTest.php @@ -196,10 +196,12 @@ public function getInvalidFormats(): \Generator yield ['199-01-1', 'date']; yield ['2012-0-11', 'date']; yield ['2012-10-1', 'date']; + yield 'Date format with value containing null byte' => ['2020-01-01\x00', 'date']; yield ['24:01:00', 'time']; yield ['00:00:60', 'time']; yield ['25:00:00', 'time']; + yield 'Time format with value containing null byte' => ['13:37:00\x00', 'time']; yield ['invalid_value_2000-05-01T12:12:12Z', 'date-time']; yield ['2000-05-01T12:12:12Z_invalid_value', 'date-time']; From 9461f15cb3d782078033cd5b3df4f6d82658b3d3 Mon Sep 17 00:00:00 2001 From: Danny van der Sluijs Date: Fri, 21 Aug 2026 11:32:52 +0200 Subject: [PATCH 2/4] fix: Remove resolved phpstan baseline entry --- phpstan-baseline.neon | 5 ----- 1 file changed, 5 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0e1bcafb..635219b3 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -105,11 +105,6 @@ parameters: count: 1 path: src/JsonSchema/Constraints/FormatConstraint.php - - - message: "#^Method JsonSchema\\\\Constraints\\\\FormatConstraint\\:\\:validateDateTime\\(\\) has no return type specified\\.$#" - count: 1 - path: src/JsonSchema/Constraints/FormatConstraint.php - - message: "#^Method JsonSchema\\\\Constraints\\\\FormatConstraint\\:\\:validateDateTime\\(\\) has parameter \\$datetime with no type specified\\.$#" count: 1 From 2ec50d84b3b4b4cde90dde4835ace0334855de34 Mon Sep 17 00:00:00 2001 From: Danny van der Sluijs Date: Fri, 21 Aug 2026 11:37:52 +0200 Subject: [PATCH 3/4] refactor: Correct test name to match subject under test --- tests/Constraints/{FormatTest.php => FormatConstraintTest.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/Constraints/{FormatTest.php => FormatConstraintTest.php} (99%) diff --git a/tests/Constraints/FormatTest.php b/tests/Constraints/FormatConstraintTest.php similarity index 99% rename from tests/Constraints/FormatTest.php rename to tests/Constraints/FormatConstraintTest.php index de847a5e..11755b52 100644 --- a/tests/Constraints/FormatTest.php +++ b/tests/Constraints/FormatConstraintTest.php @@ -8,7 +8,7 @@ use JsonSchema\Constraints\Factory; use JsonSchema\Constraints\FormatConstraint; -class FormatTest extends BaseTestCase +class FormatConstraintTest extends BaseTestCase { /** @var bool */ protected $validateSchema = true; From bf139b08cfa145bf1d172ffe1776efaa72f8bc46 Mon Sep 17 00:00:00 2001 From: Danny van der Sluijs Date: Fri, 21 Aug 2026 11:45:20 +0200 Subject: [PATCH 4/4] refactor: Process review feedback --- phpstan-baseline.neon | 5 +++++ src/JsonSchema/Constraints/FormatConstraint.php | 5 ++--- tests/Constraints/Draft06/FormatConstraintTest.php | 8 ++++---- tests/Constraints/Draft07/FormatConstraintTest.php | 8 ++++---- tests/Constraints/Draft2019/FormatConstraintTest.php | 8 ++++---- tests/Constraints/FormatConstraintTest.php | 4 ++-- 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 635219b3..0e1bcafb 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -105,6 +105,11 @@ parameters: count: 1 path: src/JsonSchema/Constraints/FormatConstraint.php + - + message: "#^Method JsonSchema\\\\Constraints\\\\FormatConstraint\\:\\:validateDateTime\\(\\) has no return type specified\\.$#" + count: 1 + path: src/JsonSchema/Constraints/FormatConstraint.php + - message: "#^Method JsonSchema\\\\Constraints\\\\FormatConstraint\\:\\:validateDateTime\\(\\) has parameter \\$datetime with no type specified\\.$#" count: 1 diff --git a/src/JsonSchema/Constraints/FormatConstraint.php b/src/JsonSchema/Constraints/FormatConstraint.php index 64d020d4..20fdb207 100644 --- a/src/JsonSchema/Constraints/FormatConstraint.php +++ b/src/JsonSchema/Constraints/FormatConstraint.php @@ -16,7 +16,6 @@ use JsonSchema\Rfc3339; use JsonSchema\Tool\Validator\RelativeReferenceValidator; use JsonSchema\Tool\Validator\UriValidator; -use Throwable; /** * Validates against the "format" property @@ -153,11 +152,11 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = } } - protected function validateDateTime($datetime, $format): bool + protected function validateDateTime($datetime, $format) { try { $dt = \DateTime::createFromFormat($format, (string) $datetime); - } catch (Throwable $e) { + } catch (\Throwable $e) { return false; } diff --git a/tests/Constraints/Draft06/FormatConstraintTest.php b/tests/Constraints/Draft06/FormatConstraintTest.php index 33992533..7be2849d 100644 --- a/tests/Constraints/Draft06/FormatConstraintTest.php +++ b/tests/Constraints/Draft06/FormatConstraintTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Constraints\Draft06; +namespace JsonSchema\Tests\Constraints\Draft06; use Generator; use JsonSchema\Constraints\Drafts\Draft06\FormatConstraint; @@ -25,10 +25,10 @@ public function testInvalidFormat($string, $format): void public function getInvalidFormats(): Generator { - yield 'Date-time format with value containing null byte' => ['2020-01-01T12:34:56\x00', 'date']; + yield 'Date-time format with value containing null byte' => ["2020-01-01T12:34:56\x00", 'date-time']; - yield 'Date format with value containing null byte' => ['2020-01-01\x00', 'date']; + yield 'Date format with value containing null byte' => ["2020-01-01\x00", 'date']; - yield 'Time format with value containing null byte' => ['13:37:00\x00', 'time']; + yield 'Time format with value containing null byte' => ["13:37:00\x00", 'time']; } } diff --git a/tests/Constraints/Draft07/FormatConstraintTest.php b/tests/Constraints/Draft07/FormatConstraintTest.php index a5ecd44e..fcf09b80 100644 --- a/tests/Constraints/Draft07/FormatConstraintTest.php +++ b/tests/Constraints/Draft07/FormatConstraintTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Constraints\Draft07; +namespace JsonSchema\Tests\Constraints\Draft07; use Generator; use JsonSchema\Constraints\Drafts\Draft07\FormatConstraint; @@ -25,10 +25,10 @@ public function testInvalidFormat($string, $format): void public function getInvalidFormats(): Generator { - yield 'Date-time format with value containing null byte' => ['2020-01-01T12:34:56\x00', 'date']; + yield 'Date-time format with value containing null byte' => ["2020-01-01T12:34:56\x00", 'date-time']; - yield 'Date format with value containing null byte' => ['2020-01-01\x00', 'date']; + yield 'Date format with value containing null byte' => ["2020-01-01\x00", 'date']; - yield 'Time format with value containing null byte' => ['13:37:00\x00', 'time']; + yield 'Time format with value containing null byte' => ["13:37:00\x00", 'time']; } } diff --git a/tests/Constraints/Draft2019/FormatConstraintTest.php b/tests/Constraints/Draft2019/FormatConstraintTest.php index 4c3420ad..5c6b90d5 100644 --- a/tests/Constraints/Draft2019/FormatConstraintTest.php +++ b/tests/Constraints/Draft2019/FormatConstraintTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Constraints\Draft2019; +namespace JsonSchema\Tests\Constraints\Draft2019; use Generator; use JsonSchema\Constraints\Drafts\Draft2019\FormatConstraint; @@ -25,10 +25,10 @@ public function testInvalidFormat($string, $format): void public function getInvalidFormats(): Generator { - yield 'Date-time format with value containing null byte' => ['2020-01-01T12:34:56\x00', 'date']; + yield 'Date-time format with value containing null byte' => ["2020-01-01T12:34:56\x00", 'date-time']; - yield 'Date format with value containing null byte' => ['2020-01-01\x00', 'date']; + yield 'Date format with value containing null byte' => ["2020-01-01\x00", 'date']; - yield 'Time format with value containing null byte' => ['13:37:00\x00', 'time']; + yield 'Time format with value containing null byte' => ["13:37:00\x00", 'time']; } } diff --git a/tests/Constraints/FormatConstraintTest.php b/tests/Constraints/FormatConstraintTest.php index 11755b52..948bfd31 100644 --- a/tests/Constraints/FormatConstraintTest.php +++ b/tests/Constraints/FormatConstraintTest.php @@ -196,12 +196,12 @@ public function getInvalidFormats(): \Generator yield ['199-01-1', 'date']; yield ['2012-0-11', 'date']; yield ['2012-10-1', 'date']; - yield 'Date format with value containing null byte' => ['2020-01-01\x00', 'date']; + yield 'Date format with value containing null byte' => ["2020-01-01\x00", 'date']; yield ['24:01:00', 'time']; yield ['00:00:60', 'time']; yield ['25:00:00', 'time']; - yield 'Time format with value containing null byte' => ['13:37:00\x00', 'time']; + yield 'Time format with value containing null byte' => ["13:37:00\x00", 'time']; yield ['invalid_value_2000-05-01T12:12:12Z', 'date-time']; yield ['2000-05-01T12:12:12Z_invalid_value', 'date-time'];