From 0cdb862a4dc83a541feba7668ca5be12ba234005 Mon Sep 17 00:00:00 2001 From: Volker Dusch Date: Wed, 10 Jun 2026 01:10:36 +0200 Subject: [PATCH 1/2] Fix uuid() accepting `{`, `}`, and prefixes inside the string Instead of stripping "urn:", "uuid:", "{" and "}" from anywhere in the value before matching. So strings like "5{}50e8400-..." or an interior "urn:" passed the assertion and the un-normalised string was returned to the caller. Moved the validated into the regex: - The prefixes are only accepted at the start - A leading "{" must be paired with a closing "}" using a PCRE conditional --- src/Assert.php | 16 +++++----------- tests/AssertTest.php | 8 ++++++++ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Assert.php b/src/Assert.php index 317cb258..089b0202 100644 --- a/src/Assert.php +++ b/src/Assert.php @@ -2442,16 +2442,10 @@ public static function uuid(mixed $value, string|callable $message = ''): string { static::string($value, $message); - $originalValue = $value; - $value = \str_replace(['urn:', 'uuid:', '{', '}'], '', $value); - - // The nil UUID is special form of UUID that is specified to have all - // 128 bits set to zero. - if ('00000000-0000-0000-0000-000000000000' === $value) { - return $originalValue; - } - - if (!\preg_match('/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/D', $value)) { + // Accepts the plain form (including the nil UUID with all 128 bits + // set to zero), optionally preceded by "urn:" and/or "uuid:" and + // optionally wrapped in a matching pair of curly braces. + if (!\preg_match('/^(?:urn:)?(?:uuid:)?(\{)?[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}(?(1)\})$/D', $value)) { $message = self::resolveMessage($message); static::reportInvalidArgument(\sprintf( $message ?: 'Value %s is not a valid UUID.', @@ -2459,7 +2453,7 @@ public static function uuid(mixed $value, string|callable $message = ''): string )); } - return $originalValue; + return $value; } /** diff --git a/tests/AssertTest.php b/tests/AssertTest.php index 6c947c5b..e7cbcb5a 100644 --- a/tests/AssertTest.php +++ b/tests/AssertTest.php @@ -588,7 +588,9 @@ public static function getTests(): array ['isNonEmptyMap', [[1, 2, 3]], false], ['uuid', ['00000000-0000-0000-0000-000000000000'], true], ['uuid', ['urn:ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], true], + ['uuid', ['urn:uuid:ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], true], ['uuid', ['uuid:{ff6f8cb0-c57d-21e1-9b21-0800200c9a66}'], true], + ['uuid', ['{ff6f8cb0-c57d-21e1-9b21-0800200c9a66}'], true], ['uuid', ['ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], true], ['uuid', ['ff6f8cb0-c57d-11e1-9b21-0800200c9a66'], true], ['uuid', ['ff6f8cb0-c57d-31e1-9b21-0800200c9a66'], true], @@ -603,6 +605,12 @@ public static function getTests(): array ['uuid', ['ff6f8cb0-c57da-51e1-9b21-0800200c9a66'], false], ['uuid', ['af6f8cb-c57d-11e1-9b21-0800200c9a66'], false], ['uuid', ['3f6f8cb0-c57d-11e1-9b21-0800200c9a6'], false], + ['uuid', ['f{}f6f8cb0-c57d-21e1-9b21-0800200c9a66'], false], + ['uuid', ['ff6f8cb0-c57d-21e1-9b21-08002urn:00c9a66'], false], + ['uuid', ['urn:ff6f8cb0-c57d-21e1-9b21-0800200c9a66uuid:'], false], + ['uuid', ['{}{}ff6f8cb0-c57d-21e1-9b21-0800200c9a66{}{}'], false], + ['uuid', ['{ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], false], + ['uuid', ['ff6f8cb0-c57d-21e1-9b21-0800200c9a66}'], false], ['throws', [function () { throw new LogicException('test'); }, 'LogicException'], true], ['throws', [function () { throw new LogicException('test'); }, 'IllogicException'], false], ['throws', [function () { throw new Exception('test'); }], true], From 75bf272218b9400788f4a95d6c61a840ad88c516 Mon Sep 17 00:00:00 2001 From: Volker Dusch Date: Mon, 15 Jun 2026 15:26:52 +0200 Subject: [PATCH 2/2] Refactoring to individual ifs as discussed in code review --- src/Assert.php | 35 +++++++++++++++++++++++++---------- tests/AssertTest.php | 3 ++- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/Assert.php b/src/Assert.php index 089b0202..31c861b9 100644 --- a/src/Assert.php +++ b/src/Assert.php @@ -2442,18 +2442,33 @@ public static function uuid(mixed $value, string|callable $message = ''): string { static::string($value, $message); - // Accepts the plain form (including the nil UUID with all 128 bits - // set to zero), optionally preceded by "urn:" and/or "uuid:" and - // optionally wrapped in a matching pair of curly braces. - if (!\preg_match('/^(?:urn:)?(?:uuid:)?(\{)?[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}(?(1)\})$/D', $value)) { - $message = self::resolveMessage($message); - static::reportInvalidArgument(\sprintf( - $message ?: 'Value %s is not a valid UUID.', - static::valueToString($value) - )); + $uuid = '[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}'; + + // URN form as specified by RFC 9562, e.g. "urn:uuid:ff6f8cb0-...". + if (\str_starts_with($value, 'urn:uuid:') && \preg_match('/^urn:uuid:'.$uuid.'$/D', $value)) { + return $value; } - return $value; + // "uuid:" prefix, optionally combined with the curly-braced form. + if (\str_starts_with($value, 'uuid:') && \preg_match('/^uuid:(?:'.$uuid.'|\{'.$uuid.'\})$/D', $value)) { + return $value; + } + + // Curly-braced form; the braces must be a matching pair. + if (\str_starts_with($value, '{') && \str_ends_with($value, '}') && \preg_match('/^\{'.$uuid.'\}$/D', $value)) { + return $value; + } + + // Plain form, including the nil UUID with all 128 bits set to zero. + if (\preg_match('/^'.$uuid.'$/D', $value)) { + return $value; + } + + $message = self::resolveMessage($message); + static::reportInvalidArgument(\sprintf( + $message ?: 'Value %s is not a valid UUID.', + static::valueToString($value) + )); } /** diff --git a/tests/AssertTest.php b/tests/AssertTest.php index e7cbcb5a..f0ec064d 100644 --- a/tests/AssertTest.php +++ b/tests/AssertTest.php @@ -587,7 +587,6 @@ public static function getTests(): array ['isNonEmptyMap', [[]], false], ['isNonEmptyMap', [[1, 2, 3]], false], ['uuid', ['00000000-0000-0000-0000-000000000000'], true], - ['uuid', ['urn:ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], true], ['uuid', ['urn:uuid:ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], true], ['uuid', ['uuid:{ff6f8cb0-c57d-21e1-9b21-0800200c9a66}'], true], ['uuid', ['{ff6f8cb0-c57d-21e1-9b21-0800200c9a66}'], true], @@ -608,6 +607,8 @@ public static function getTests(): array ['uuid', ['f{}f6f8cb0-c57d-21e1-9b21-0800200c9a66'], false], ['uuid', ['ff6f8cb0-c57d-21e1-9b21-08002urn:00c9a66'], false], ['uuid', ['urn:ff6f8cb0-c57d-21e1-9b21-0800200c9a66uuid:'], false], + ['uuid', ['urn:ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], false], + ['uuid', ['urn:uuid:{ff6f8cb0-c57d-21e1-9b21-0800200c9a66}'], false], ['uuid', ['{}{}ff6f8cb0-c57d-21e1-9b21-0800200c9a66{}{}'], false], ['uuid', ['{ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], false], ['uuid', ['ff6f8cb0-c57d-21e1-9b21-0800200c9a66}'], false],