From adc1f7e094859e099b898a7332372448b94d6c1c Mon Sep 17 00:00:00 2001 From: seethinajayadileep Date: Thu, 5 Mar 2026 12:09:36 +0530 Subject: [PATCH 1/2] Fix silent overflow when coercing JSON numbers to integral types --- .../org/openqa/selenium/json/JsonInput.java | 2 +- .../openqa/selenium/json/NumberCoercer.java | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/java/src/org/openqa/selenium/json/JsonInput.java b/java/src/org/openqa/selenium/json/JsonInput.java index 99c79373db482..f975f72fce768 100644 --- a/java/src/org/openqa/selenium/json/JsonInput.java +++ b/java/src/org/openqa/selenium/json/JsonInput.java @@ -263,7 +263,7 @@ public Number nextNumber() { return Long.valueOf(builder.toString()); } - return new BigDecimal(builder.toString()).doubleValue(); + return new BigDecimal(builder.toString()); } catch (NumberFormatException e) { throw new JsonException("Unable to parse to a number: " + builder + ". " + input, e); } diff --git a/java/src/org/openqa/selenium/json/NumberCoercer.java b/java/src/org/openqa/selenium/json/NumberCoercer.java index 748305dcad9c3..1615b34197921 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,51 @@ 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) { + // Only for integral targets + if (!(stereotype == Byte.class + || stereotype == Short.class + || stereotype == Integer.class + || stereotype == Long.class)) { + return; + } + + final BigInteger value; + try { + // Parses "2147483648", "1e3", "1.0" safely; rejects "1.2" + 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); + } + } } From 5ad714bb20361e977141778c41cbfea5e4f4dfb5 Mon Sep 17 00:00:00 2001 From: seethinajayadileep Date: Thu, 5 Mar 2026 13:23:07 +0530 Subject: [PATCH 2/2] Fix numeric overflow in JSON integral coercion and add tests --- .../org/openqa/selenium/json/JsonInput.java | 2 +- .../openqa/selenium/json/NumberCoercer.java | 6 ++++-- .../org/openqa/selenium/json/JsonTest.java | 20 +++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/java/src/org/openqa/selenium/json/JsonInput.java b/java/src/org/openqa/selenium/json/JsonInput.java index f975f72fce768..99c79373db482 100644 --- a/java/src/org/openqa/selenium/json/JsonInput.java +++ b/java/src/org/openqa/selenium/json/JsonInput.java @@ -263,7 +263,7 @@ public Number nextNumber() { return Long.valueOf(builder.toString()); } - return new BigDecimal(builder.toString()); + return new BigDecimal(builder.toString()).doubleValue(); } catch (NumberFormatException e) { throw new JsonException("Unable to parse to a number: " + builder + ". " + input, e); } diff --git a/java/src/org/openqa/selenium/json/NumberCoercer.java b/java/src/org/openqa/selenium/json/NumberCoercer.java index 1615b34197921..e85c957723de2 100644 --- a/java/src/org/openqa/selenium/json/NumberCoercer.java +++ b/java/src/org/openqa/selenium/json/NumberCoercer.java @@ -88,7 +88,9 @@ public BiFunction apply(Type ignored) { } private static void validateIntegralRange(Number number, Class stereotype) { - // Only for integral targets + // 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 @@ -98,7 +100,7 @@ private static void validateIntegralRange(Number number, Class stereotype) { final BigInteger value; try { - // Parses "2147483648", "1e3", "1.0" safely; rejects "1.2" + // 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(); 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")));