diff --git a/java/src/org/openqa/selenium/json/NumberCoercer.java b/java/src/org/openqa/selenium/json/NumberCoercer.java index 748305dcad9c3..e85c957723de2 100644 --- a/java/src/org/openqa/selenium/json/NumberCoercer.java +++ b/java/src/org/openqa/selenium/json/NumberCoercer.java @@ -19,6 +19,8 @@ import java.io.StringReader; import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; @@ -80,7 +82,53 @@ public BiFunction apply(Type ignored) { default: throw new JsonException("Unable to coerce to a number: " + jsonInput.peek()); } + validateIntegralRange(number, stereotype); return mapper.apply(number); }; } + + private static void validateIntegralRange(Number number, Class stereotype) { + // Prevent silent overflow when JSON numbers are coerced to integral types. + // Java's Number.intValue()/longValue() silently wraps values outside the + // valid range, which previously produced incorrect results instead of errors. + if (!(stereotype == Byte.class + || stereotype == Short.class + || stereotype == Integer.class + || stereotype == Long.class)) { + return; + } + + final BigInteger value; + try { + // Use BigDecimal so we can reject fractional values and validate range before coercion. + BigDecimal bd = + (number instanceof BigDecimal) ? (BigDecimal) number : new BigDecimal(number.toString()); + value = bd.toBigIntegerExact(); + } catch (RuntimeException e) { + throw new JsonException( + "Expected an integer value for " + stereotype.getSimpleName() + ": " + number, e); + } + + BigInteger min; + BigInteger max; + + if (stereotype == Byte.class) { + min = BigInteger.valueOf(Byte.MIN_VALUE); + max = BigInteger.valueOf(Byte.MAX_VALUE); + } else if (stereotype == Short.class) { + min = BigInteger.valueOf(Short.MIN_VALUE); + max = BigInteger.valueOf(Short.MAX_VALUE); + } else if (stereotype == Integer.class) { + min = BigInteger.valueOf(Integer.MIN_VALUE); + max = BigInteger.valueOf(Integer.MAX_VALUE); + } else { // Long.class + min = BigInteger.valueOf(Long.MIN_VALUE); + max = BigInteger.valueOf(Long.MAX_VALUE); + } + + if (value.compareTo(min) < 0 || value.compareTo(max) > 0) { + throw new JsonException( + "Numeric value out of range for " + stereotype.getSimpleName() + ": " + value); + } + } } diff --git a/java/test/org/openqa/selenium/json/JsonTest.java b/java/test/org/openqa/selenium/json/JsonTest.java index a8470b722f51b..d87d1e2c1670f 100644 --- a/java/test/org/openqa/selenium/json/JsonTest.java +++ b/java/test/org/openqa/selenium/json/JsonTest.java @@ -72,6 +72,26 @@ void canReadANumber() { assertThat((Double) new Json().toType("4.2e-1", Double.class)).isEqualTo(0.42); } + @Test + void shouldRejectIntegerOverflow() { + Json json = new Json(); + + assertThatExceptionOfType(JsonException.class) + .isThrownBy(() -> json.toType("2147483648", Integer.class)) + .withMessageContaining("out of range") + .withMessageContaining("Integer"); + } + + @Test + void shouldRejectFractionalValueForInteger() { + Json json = new Json(); + + assertThatExceptionOfType(JsonException.class) + .isThrownBy(() -> json.toType("1.2", Integer.class)) + .withMessageContaining("Expected an integer value") + .withMessageContaining("Integer"); + } + @Test void canRoundTripNumbers() { Map original = Map.of("options", Map.of("args", List.of(1L, "hello")));