diff --git a/src/Symfony/Component/JsonPath/JsonCrawler.php b/src/Symfony/Component/JsonPath/JsonCrawler.php index 0793a5c5d7b1..d66b328a7149 100644 --- a/src/Symfony/Component/JsonPath/JsonCrawler.php +++ b/src/Symfony/Component/JsonPath/JsonCrawler.php @@ -143,6 +143,10 @@ private function evaluateBracket(string $expr, mixed $value): array // single negative index if (preg_match('/^-\d+$/', $expr)) { + if (JsonPathUtils::hasLeadingZero($expr) || JsonPathUtils::isIntegerOverflow($expr) || '-0' === $expr) { + throw new JsonCrawlerException($expr, 'invalid index selector'); + } + if (!array_is_list($value)) { return []; } @@ -154,6 +158,12 @@ private function evaluateBracket(string $expr, mixed $value): array // start and end index if (preg_match('/^-?\d+(?:\s*,\s*-?\d+)*$/', $expr)) { + foreach (explode(',', $expr) as $exprPart) { + if (JsonPathUtils::hasLeadingZero($exprPart = trim($exprPart)) || JsonPathUtils::isIntegerOverflow($exprPart) || '-0' === $exprPart) { + throw new JsonCrawlerException($expr, 'invalid index selector'); + } + } + if (!array_is_list($value)) { return []; } @@ -172,17 +182,41 @@ private function evaluateBracket(string $expr, mixed $value): array return $result; } - if (preg_match('/^(-?\d*+)\s*+:\s*+(-?\d*+)(?:\s*+:\s*+(-?\d++))?$/', $expr, $matches)) { + if (preg_match('/^(-?\d*+)\s*+:\s*+(-?\d*+)(?:\s*+:\s*+(-?\d*+))?$/', $expr, $matches)) { if (!array_is_list($value)) { return []; } + $startStr = trim($matches[1]); + $endStr = trim($matches[2]); + $stepStr = trim($matches[3] ?? '1'); + + if ( + JsonPathUtils::hasLeadingZero($startStr) + || JsonPathUtils::hasLeadingZero($endStr) + || JsonPathUtils::hasLeadingZero($stepStr) + ) { + throw new JsonCrawlerException($expr, 'slice selector numbers cannot have leading zeros'); + } + + if ('-0' === $startStr || '-0' === $endStr || '-0' === $stepStr) { + throw new JsonCrawlerException($expr, 'slice selector cannot contain negative zero'); + } + + if ( + JsonPathUtils::isIntegerOverflow($startStr) + || JsonPathUtils::isIntegerOverflow($endStr) + || JsonPathUtils::isIntegerOverflow($stepStr) + ) { + throw new JsonCrawlerException($expr, 'slice selector integer overflow'); + } + $length = \count($value); - $start = '' !== $matches[1] ? (int) $matches[1] : null; - $end = '' !== $matches[2] ? (int) $matches[2] : null; - $step = isset($matches[3]) && '' !== $matches[3] ? (int) $matches[3] : 1; + $start = '' !== $startStr ? (int) $startStr : null; + $end = '' !== $endStr ? (int) $endStr : null; + $step = '' !== $stepStr ? (int) $stepStr : 1; - if (0 === $step || $start > $length) { + if (0 === $step) { return []; } @@ -192,6 +226,11 @@ private function evaluateBracket(string $expr, mixed $value): array if ($start < 0) { $start = $length + $start; } + + if ($step > 0 && $start >= $length) { + return []; + } + $start = max(0, min($start, $length - 1)); } diff --git a/src/Symfony/Component/JsonPath/JsonPathUtils.php b/src/Symfony/Component/JsonPath/JsonPathUtils.php index 30bf446b6a9d..998ff74e18c3 100644 --- a/src/Symfony/Component/JsonPath/JsonPathUtils.php +++ b/src/Symfony/Component/JsonPath/JsonPathUtils.php @@ -227,4 +227,34 @@ public static function parseCommaSeparatedValues(string $expr): array return $parts; } + + public static function hasLeadingZero(string $s): bool + { + if ('' === $s || str_starts_with($s, '-') && '' === $s = substr($s, 1)) { + return false; + } + + return '0' === $s[0] && 1 < \strlen($s); + } + + /** + * Safe integer range is [-(2^53) + 1, (2^53) - 1]. + * + * @see https://datatracker.ietf.org/doc/rfc9535/, section 2.1 + */ + public static function isIntegerOverflow(string $s): bool + { + if ('' === $s) { + return false; + } + + if (\strlen($s) > (str_starts_with($s, '-') ? 17 : 16)) { + // obviously too long to represent anything in the interval + return true; + } + + $value = (int) $s; + + return $value < (-(1 << 53) + 1) || ((1 << 53) - 1) < $value; + } } diff --git a/src/Symfony/Component/JsonPath/Tests/JsonPathComplianceTestSuiteTest.php b/src/Symfony/Component/JsonPath/Tests/JsonPathComplianceTestSuiteTest.php index b39b68abcd46..a04e9c24fca1 100644 --- a/src/Symfony/Component/JsonPath/Tests/JsonPathComplianceTestSuiteTest.php +++ b/src/Symfony/Component/JsonPath/Tests/JsonPathComplianceTestSuiteTest.php @@ -103,11 +103,6 @@ final class JsonPathComplianceTestSuiteTest extends TestCase 'filter, group terms, right', 'name selector, double quotes, escaped reverse solidus', 'name selector, single quotes, escaped reverse solidus', - 'slice selector, slice selector with everything omitted, long form', - 'slice selector, start, min exact', - 'slice selector, start, max exact', - 'slice selector, end, min exact', - 'slice selector, end, max exact', 'basic, descendant segment, multiple selectors', 'basic, bald descendant segment', 'filter, relative non-singular query, index, equal', @@ -142,12 +137,6 @@ final class JsonPathComplianceTestSuiteTest extends TestCase 'filter, absolute non-singular query, slice, less-or-equal', 'filter, equals, special nothing', 'filter, group terms, left', - 'index selector, min exact index - 1', - 'index selector, max exact index + 1', - 'index selector, overflowing index', - 'index selector, leading 0', - 'index selector, -0', - 'index selector, leading -0', 'name selector, double quotes, escaped line feed', 'name selector, double quotes, invalid escaped single quote', 'name selector, double quotes, question mark escape', @@ -172,18 +161,6 @@ final class JsonPathComplianceTestSuiteTest extends TestCase 'name selector, single quotes, escaped backspace', 'name selector, single quotes, escaped line feed', 'name selector, single quotes, invalid escaped double quote', - 'slice selector, excessively large from value with negative step', - 'slice selector, step, min exact - 1', - 'slice selector, step, max exact + 1', - 'slice selector, overflowing to value', - 'slice selector, underflowing from value', - 'slice selector, overflowing from value with negative step', - 'slice selector, underflowing to value with negative step', - 'slice selector, overflowing step', - 'slice selector, underflowing step', - 'slice selector, step, leading 0', - 'slice selector, step, -0', - 'slice selector, step, leading -0', ]; /**