From e59ae794c765757de5db21c3b217785dbe87fb49 Mon Sep 17 00:00:00 2001 From: Naveed Khan Date: Sat, 5 Sep 2026 15:05:15 +0530 Subject: [PATCH] fix range checks bypassed by wraparound in number converters the byte/short/integer/long branches of NumberConverter.toNumber and the integer locale converters compared longValue()/doubleValue() of the source, which wrap for BigInteger/BigDecimal and round at the 2^63 boundary for double, letting out-of-range values convert to wrong in-range results (2^63 becomes Long.MIN_VALUE, 2^64 + 5 becomes 5); compare the exact value as BigDecimal before narrowing --- .../converters/NumberConverter.java | 88 ++++++++++++------- .../converters/ByteLocaleConverter.java | 4 +- .../converters/DecimalLocaleConverter.java | 32 +++++++ .../converters/IntegerLocaleConverter.java | 4 +- .../converters/LongLocaleConverter.java | 2 +- .../converters/ShortLocaleConverter.java | 4 +- .../converters/ByteConverterTest.java | 17 ++++ .../converters/ByteLocaleConverterTest.java | 16 ++++ .../converters/IntegerConverterTest.java | 15 ++++ .../IntegerLocaleConverterTest.java | 15 ++++ .../converters/LongConverterTest.java | 32 +++++++ .../converters/LongLocaleConverterTest.java | 16 ++++ .../converters/ShortConverterTest.java | 15 ++++ .../converters/ShortLocaleConverterTest.java | 16 ++++ 14 files changed, 238 insertions(+), 38 deletions(-) diff --git a/src/main/java/org/apache/commons/beanutils2/converters/NumberConverter.java b/src/main/java/org/apache/commons/beanutils2/converters/NumberConverter.java index 04f5b14fe..e43c98262 100644 --- a/src/main/java/org/apache/commons/beanutils2/converters/NumberConverter.java +++ b/src/main/java/org/apache/commons/beanutils2/converters/NumberConverter.java @@ -317,6 +317,59 @@ public void setUseLocaleFormat(final boolean useLocaleFormat) { this.useLocaleFormat = useLocaleFormat; } + /** + * Converts a {@code Number} to a {@code long}, validating that its whole part is within the specified range. + *

+ * The range test must not be performed on a narrowed copy of the value: {@code longValue()} of a {@link BigInteger} or {@link BigDecimal} keeps only the + * low-order 64 bits and a {@code double} cannot represent every {@code long}, so an out-of-range value can wrap or round into range before a {@code long} + * or {@code double} based bounds check sees it. These types are therefore compared as {@link BigDecimal}. + * + * @param sourceType The type being converted from + * @param targetType The Number type to convert to + * @param value The Number to convert. + * @param min The smallest value of the target type + * @param max The largest value of the target type + * @return The value as a {@code long}, with any fractional part discarded. + * @throws ConversionException if the value is outside the specified range. + */ + private long toLong(final Class sourceType, final Class targetType, final Number value, final long min, final long max) { + BigDecimal decimalValue = null; + if (value instanceof BigDecimal) { + decimalValue = (BigDecimal) value; + } else if (value instanceof BigInteger) { + decimalValue = new BigDecimal((BigInteger) value); + } else if (value instanceof Float || value instanceof Double) { + final double doubleValue = value.doubleValue(); + if (doubleValue == Double.POSITIVE_INFINITY) { + throw ConversionException.format("%s value '%s' is too large for %s", toString(sourceType), value, toString(targetType)); + } + if (doubleValue == Double.NEGATIVE_INFINITY) { + throw ConversionException.format("%s value '%s' is too small %s", toString(sourceType), value, toString(targetType)); + } + if (!Double.isNaN(doubleValue)) { + decimalValue = new BigDecimal(doubleValue); + } + } + if (decimalValue != null) { + // Values whose whole part truncates into range stay accepted, so compare against min - 1 and max + 1. + if (decimalValue.compareTo(BigDecimal.valueOf(max).add(BigDecimal.ONE)) >= 0) { + throw ConversionException.format("%s value '%s' is too large for %s", toString(sourceType), value, toString(targetType)); + } + if (decimalValue.compareTo(BigDecimal.valueOf(min).subtract(BigDecimal.ONE)) <= 0) { + throw ConversionException.format("%s value '%s' is too small %s", toString(sourceType), value, toString(targetType)); + } + return decimalValue.longValue(); + } + final long longValue = value.longValue(); + if (longValue > max) { + throw ConversionException.format("%s value '%s' is too large for %s", toString(sourceType), value, toString(targetType)); + } + if (longValue < min) { + throw ConversionException.format("%s value '%s' is too small %s", toString(sourceType), value, toString(targetType)); + } + return longValue; + } + /** * Default String to Number conversion. *

@@ -413,49 +466,22 @@ private T toNumber(final Class sourceType, final Class targetType, fin // Byte if (targetType.equals(Byte.class)) { - final long longValue = value.longValue(); - if (longValue > Byte.MAX_VALUE) { - throw ConversionException.format("%s value '%s' is too large for %s", toString(sourceType), value, toString(targetType)); - } - if (longValue < Byte.MIN_VALUE) { - throw ConversionException.format("%s value '%s' is too small %s", toString(sourceType), value, toString(targetType)); - } - return targetType.cast(Byte.valueOf(value.byteValue())); + return targetType.cast(Byte.valueOf((byte) toLong(sourceType, targetType, value, Byte.MIN_VALUE, Byte.MAX_VALUE))); } // Short if (targetType.equals(Short.class)) { - final long longValue = value.longValue(); - if (longValue > Short.MAX_VALUE) { - throw ConversionException.format("%s value '%s' is too large for %s", toString(sourceType), value, toString(targetType)); - } - if (longValue < Short.MIN_VALUE) { - throw ConversionException.format("%s value '%s' is too small %s", toString(sourceType), value, toString(targetType)); - } - return targetType.cast(Short.valueOf(value.shortValue())); + return targetType.cast(Short.valueOf((short) toLong(sourceType, targetType, value, Short.MIN_VALUE, Short.MAX_VALUE))); } // Integer if (targetType.equals(Integer.class)) { - final long longValue = value.longValue(); - if (longValue > Integer.MAX_VALUE) { - throw ConversionException.format("%s value '%s' is too large for %s", toString(sourceType), value, toString(targetType)); - } - if (longValue < Integer.MIN_VALUE) { - throw ConversionException.format("%s value '%s' is too small %s", toString(sourceType), value, toString(targetType)); - } - return targetType.cast(Integer.valueOf(value.intValue())); + return targetType.cast(Integer.valueOf((int) toLong(sourceType, targetType, value, Integer.MIN_VALUE, Integer.MAX_VALUE))); } // Long if (targetType.equals(Long.class)) { - if (value.doubleValue() > Long.MAX_VALUE) { - throw ConversionException.format("%s value '%s' is too large for %s", toString(sourceType), value, toString(targetType)); - } - if (value.doubleValue() < Long.MIN_VALUE) { - throw ConversionException.format("%s value '%s' is too small %s", toString(sourceType), value, toString(targetType)); - } - return targetType.cast(Long.valueOf(value.longValue())); + return targetType.cast(Long.valueOf(toLong(sourceType, targetType, value, Long.MIN_VALUE, Long.MAX_VALUE))); } // Float diff --git a/src/main/java/org/apache/commons/beanutils2/locale/converters/ByteLocaleConverter.java b/src/main/java/org/apache/commons/beanutils2/locale/converters/ByteLocaleConverter.java index 0409f7952..ab93f101f 100644 --- a/src/main/java/org/apache/commons/beanutils2/locale/converters/ByteLocaleConverter.java +++ b/src/main/java/org/apache/commons/beanutils2/locale/converters/ByteLocaleConverter.java @@ -71,8 +71,8 @@ private ByteLocaleConverter(final Byte defaultValue, final Locale locale, final @Override protected Byte parse(final Object value, final String pattern) throws ParseException { final Number parsed = super.parse(value, pattern); - if (parsed.longValue() != parsed.byteValue()) { - throw new ConversionException("Supplied number is not of type Byte: " + parsed.longValue()); + if (parsed.longValue() != parsed.byteValue() || !inRange(parsed, Byte.MIN_VALUE, Byte.MAX_VALUE)) { + throw new ConversionException("Supplied number is not of type Byte: " + parsed); } // now returns property Byte return Byte.valueOf(checkInteger(parsed).byteValue()); diff --git a/src/main/java/org/apache/commons/beanutils2/locale/converters/DecimalLocaleConverter.java b/src/main/java/org/apache/commons/beanutils2/locale/converters/DecimalLocaleConverter.java index accf9aaa2..0e634e995 100644 --- a/src/main/java/org/apache/commons/beanutils2/locale/converters/DecimalLocaleConverter.java +++ b/src/main/java/org/apache/commons/beanutils2/locale/converters/DecimalLocaleConverter.java @@ -17,6 +17,8 @@ package org.apache.commons.beanutils2.locale.converters; +import java.math.BigDecimal; +import java.math.BigInteger; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; @@ -104,6 +106,36 @@ Number checkInteger(final Number number) { return number; } + /** + * Tests whether the whole part of the given number is within the specified range. + *

+ * The test must not be performed on a narrowed copy of the value: {@code longValue()} of a {@link BigInteger} or {@link BigDecimal} keeps only the + * low-order 64 bits and a {@code double} cannot represent every {@code long}, so an out-of-range value can wrap or round into range before a narrowed + * bounds check sees it. These types are therefore compared as {@link BigDecimal}; other types compare their {@code longValue()}. + * + * @param number The number to test. + * @param min The smallest value of the target type. + * @param max The largest value of the target type. + * @return {@code true} if the whole part of the number is within the range. + */ + boolean inRange(final Number number, final long min, final long max) { + BigDecimal decimalValue = null; + if (number instanceof BigDecimal) { + decimalValue = (BigDecimal) number; + } else if (number instanceof BigInteger) { + decimalValue = new BigDecimal((BigInteger) number); + } else if ((number instanceof Float || number instanceof Double) && Double.isFinite(number.doubleValue())) { + decimalValue = new BigDecimal(number.doubleValue()); + } + if (decimalValue != null) { + // Values whose whole part truncates into range stay accepted, so compare against min - 1 and max + 1. + return decimalValue.compareTo(BigDecimal.valueOf(max).add(BigDecimal.ONE)) < 0 + && decimalValue.compareTo(BigDecimal.valueOf(min).subtract(BigDecimal.ONE)) > 0; + } + final long longValue = number.longValue(); + return longValue <= max && longValue >= min; + } + /** * Tests whether the underlying {@link DecimalFormat} should parse into a {@link java.math.BigDecimal} so that magnitude and precision are preserved. * Subclasses that build {@link java.math.BigInteger} or {@link java.math.BigDecimal} values override this to return {@code true}; the narrowing converters diff --git a/src/main/java/org/apache/commons/beanutils2/locale/converters/IntegerLocaleConverter.java b/src/main/java/org/apache/commons/beanutils2/locale/converters/IntegerLocaleConverter.java index b90a13855..55167ec60 100644 --- a/src/main/java/org/apache/commons/beanutils2/locale/converters/IntegerLocaleConverter.java +++ b/src/main/java/org/apache/commons/beanutils2/locale/converters/IntegerLocaleConverter.java @@ -71,8 +71,8 @@ private IntegerLocaleConverter(final Integer defaultValue, final Locale locale, @Override protected Integer parse(final Object value, final String pattern) throws ParseException { final Number parsed = super.parse(value, pattern); - if (parsed.longValue() != parsed.intValue()) { - throw new ConversionException("Supplied number is not of type Integer: " + parsed.longValue()); + if (parsed.longValue() != parsed.intValue() || !inRange(parsed, Integer.MIN_VALUE, Integer.MAX_VALUE)) { + throw new ConversionException("Supplied number is not of type Integer: " + parsed); } return Integer.valueOf(checkInteger(parsed).intValue()); // unlike superclass it will return proper Integer } diff --git a/src/main/java/org/apache/commons/beanutils2/locale/converters/LongLocaleConverter.java b/src/main/java/org/apache/commons/beanutils2/locale/converters/LongLocaleConverter.java index d01286c69..076d494e4 100644 --- a/src/main/java/org/apache/commons/beanutils2/locale/converters/LongLocaleConverter.java +++ b/src/main/java/org/apache/commons/beanutils2/locale/converters/LongLocaleConverter.java @@ -76,7 +76,7 @@ protected Long parse(final Object value, final String pattern) throws ParseExcep return (Long) result; } final double doubleValue = result.doubleValue(); - if (doubleValue < Long.MIN_VALUE || doubleValue > Long.MAX_VALUE) { + if (doubleValue < Long.MIN_VALUE || doubleValue > Long.MAX_VALUE || !inRange(result, Long.MIN_VALUE, Long.MAX_VALUE)) { throw new ConversionException("Supplied number is not of type Long: " + result); } return Long.valueOf(checkInteger(result).longValue()); diff --git a/src/main/java/org/apache/commons/beanutils2/locale/converters/ShortLocaleConverter.java b/src/main/java/org/apache/commons/beanutils2/locale/converters/ShortLocaleConverter.java index aeeab7cc9..c776687ae 100644 --- a/src/main/java/org/apache/commons/beanutils2/locale/converters/ShortLocaleConverter.java +++ b/src/main/java/org/apache/commons/beanutils2/locale/converters/ShortLocaleConverter.java @@ -81,8 +81,8 @@ protected Short parse(final Object value, final String pattern) throws ParseExce return (Short) result; } final Number parsed = (Number) result; - if (parsed.longValue() != parsed.shortValue()) { - throw new ConversionException("Supplied number is not of type Short: " + parsed.longValue()); + if (parsed.longValue() != parsed.shortValue() || !inRange(parsed, Short.MIN_VALUE, Short.MAX_VALUE)) { + throw new ConversionException("Supplied number is not of type Short: " + parsed); } // now returns property Short return Short.valueOf(checkInteger(parsed).shortValue()); diff --git a/src/test/java/org/apache/commons/beanutils2/converters/ByteConverterTest.java b/src/test/java/org/apache/commons/beanutils2/converters/ByteConverterTest.java index 0819ef631..1b9413ab9 100644 --- a/src/test/java/org/apache/commons/beanutils2/converters/ByteConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/converters/ByteConverterTest.java @@ -20,6 +20,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.math.BigDecimal; +import java.math.BigInteger; + import org.apache.commons.beanutils2.ConversionException; import org.apache.commons.beanutils2.Converter; import org.junit.jupiter.api.AfterEach; @@ -83,6 +86,20 @@ void testInvalidAmount() { assertThrows(ConversionException.class, () -> converter.convert(clazz, maxPlusOne), "More than maximum, expected ConversionException"); } + /** + * A {@link BigInteger} or {@link BigDecimal} beyond long range wraps to its low-order 64 bits in {@code longValue()}, so it can slip through a long-based + * bounds check and convert to an unrelated in-range value (2^64 + 5 converted to 5); it must be rejected. + */ + @Test + void testWrappedAmount() { + final Converter converter = makeConverter(); + final Class clazz = Byte.class; + final BigInteger wrapped = BigInteger.ONE.shiftLeft(64).add(BigInteger.valueOf(5)); + assertThrows(ConversionException.class, () -> converter.convert(clazz, wrapped), "2^64 + 5, expected ConversionException"); + assertThrows(ConversionException.class, () -> converter.convert(clazz, new BigDecimal(wrapped)), "2^64 + 5, expected ConversionException"); + assertThrows(ConversionException.class, () -> converter.convert(clazz, wrapped.negate()), "-(2^64 + 5), expected ConversionException"); + } + @Test void testSimpleConversion() throws Exception { final String[] message = { "from String", "from String", "from String", "from String", "from String", "from String", "from String", "from Byte", diff --git a/src/test/java/org/apache/commons/beanutils2/converters/ByteLocaleConverterTest.java b/src/test/java/org/apache/commons/beanutils2/converters/ByteLocaleConverterTest.java index 383d24fe0..0a6f56a73 100644 --- a/src/test/java/org/apache/commons/beanutils2/converters/ByteLocaleConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/converters/ByteLocaleConverterTest.java @@ -17,6 +17,11 @@ package org.apache.commons.beanutils2.converters; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.math.BigInteger; + +import org.apache.commons.beanutils2.ConversionException; import org.apache.commons.beanutils2.locale.converters.ByteLocaleConverter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -205,4 +210,15 @@ void testNonIntegerRejected() { converter = ByteLocaleConverter.builder().setDefault(defaultValue).setLocale(defaultLocale).get(); convertValueNoPattern(converter, "non-integer", "5.5", defaultValue); } + + /** + * A {@link BigInteger} beyond long range wraps to its low-order 64 bits in {@code longValue()}, so it can slip through a long-based range check and + * convert to an unrelated in-range value (2^64 + 5 converted to 5); it must be rejected. + */ + @Test + void testWrappedOutOfRangeRejected() { + converter = ByteLocaleConverter.builder().setLocale(defaultLocale).get(); + final BigInteger wrapped = BigInteger.ONE.shiftLeft(64).add(BigInteger.valueOf(5)); + assertThrows(ConversionException.class, () -> converter.convert(wrapped), "2^64 + 5, expected ConversionException"); + } } diff --git a/src/test/java/org/apache/commons/beanutils2/converters/IntegerConverterTest.java b/src/test/java/org/apache/commons/beanutils2/converters/IntegerConverterTest.java index c3aac81fb..25b49e6cc 100644 --- a/src/test/java/org/apache/commons/beanutils2/converters/IntegerConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/converters/IntegerConverterTest.java @@ -20,6 +20,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.math.BigInteger; + import org.apache.commons.beanutils2.ConversionException; import org.apache.commons.beanutils2.Converter; import org.junit.jupiter.api.AfterEach; @@ -83,6 +85,19 @@ void testInvalidAmount() { assertThrows(ConversionException.class, () -> converter.convert(clazz, maxPlusOne), "More than maximum, expected ConversionException"); } + /** + * A {@link BigInteger} beyond long range wraps to its low-order 64 bits in {@code longValue()}, so it can slip through a long-based bounds check and + * convert to an unrelated in-range value (2^64 + 5 converted to 5); it must be rejected. + */ + @Test + void testWrappedAmount() { + final Converter converter = makeConverter(); + final Class clazz = Integer.class; + final BigInteger wrapped = BigInteger.ONE.shiftLeft(64).add(BigInteger.valueOf(5)); + assertThrows(ConversionException.class, () -> converter.convert(clazz, wrapped), "2^64 + 5, expected ConversionException"); + assertThrows(ConversionException.class, () -> converter.convert(clazz, wrapped.negate()), "-(2^64 + 5), expected ConversionException"); + } + /** * Tests whether an invalid default object causes an exception. */ diff --git a/src/test/java/org/apache/commons/beanutils2/converters/IntegerLocaleConverterTest.java b/src/test/java/org/apache/commons/beanutils2/converters/IntegerLocaleConverterTest.java index 23981a01b..c2e0b7ebc 100644 --- a/src/test/java/org/apache/commons/beanutils2/converters/IntegerLocaleConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/converters/IntegerLocaleConverterTest.java @@ -18,7 +18,11 @@ package org.apache.commons.beanutils2.converters; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.math.BigInteger; + +import org.apache.commons.beanutils2.ConversionException; import org.apache.commons.beanutils2.locale.converters.IntegerLocaleConverter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -201,6 +205,17 @@ void testNonIntegerRejected() { convertValueNoPattern(converter, "non-integer", "5.5", defaultValue); } + /** + * A {@link BigInteger} beyond long range wraps to its low-order 64 bits in {@code longValue()}, so it can slip through a long-based range check and + * convert to an unrelated in-range value (2^64 + 5 converted to 5); it must be rejected. + */ + @Test + void testWrappedOutOfRangeRejected() { + converter = IntegerLocaleConverter.builder().setLocale(defaultLocale).get(); + final BigInteger wrapped = BigInteger.ONE.shiftLeft(64).add(BigInteger.valueOf(5)); + assertThrows(ConversionException.class, () -> converter.convert(wrapped), "2^64 + 5, expected ConversionException"); + } + /** * Test Converting a number */ diff --git a/src/test/java/org/apache/commons/beanutils2/converters/LongConverterTest.java b/src/test/java/org/apache/commons/beanutils2/converters/LongConverterTest.java index c4c828133..190722172 100644 --- a/src/test/java/org/apache/commons/beanutils2/converters/LongConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/converters/LongConverterTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.math.BigDecimal; import java.math.BigInteger; import java.util.Locale; @@ -93,6 +94,37 @@ void testLocaleStringOutOfRange() { assertThrows(ConversionException.class, () -> converter.convert(Long.class, "99999999999999999999"), "More than maximum, expected ConversionException"); } + /** + * A locale-parsed String one past {@link Long#MAX_VALUE} comes back from {@link java.text.DecimalFormat} as the {@link Double} 2^63, which a double-based + * bounds check cannot distinguish from {@link Long#MAX_VALUE}; it must be rejected rather than clamped. + */ + @Test + void testLocaleStringOutOfRangeBoundary() { + final LongConverter converter = makeConverter(); + converter.setLocale(Locale.US); + assertThrows(ConversionException.class, () -> converter.convert(Long.class, "9223372036854775808"), "One more than maximum, expected ConversionException"); + } + + /** + * Values just past the long range must not wrap or round into range before the bounds check sees them: {@code longValue()} of a {@link BigInteger} keeps + * only the low-order 64 bits (so 2^63 becomes {@link Long#MIN_VALUE}) and {@code doubleValue()} of 2^63 equals the double representation of + * {@link Long#MAX_VALUE}. + */ + @Test + void testOutOfRangeBoundary() { + final Converter converter = makeConverter(); + final Class clazz = Long.class; + final BigInteger maxPlusOne = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE); + final BigInteger minMinusOne = BigInteger.valueOf(Long.MIN_VALUE).subtract(BigInteger.ONE); + assertThrows(ConversionException.class, () -> converter.convert(clazz, maxPlusOne), "One more than maximum, expected ConversionException"); + assertThrows(ConversionException.class, () -> converter.convert(clazz, minMinusOne), "One less than minimum, expected ConversionException"); + assertThrows(ConversionException.class, () -> converter.convert(clazz, new BigDecimal(maxPlusOne)), "One more than maximum, expected ConversionException"); + assertThrows(ConversionException.class, () -> converter.convert(clazz, Double.valueOf(9.223372036854775808E18)), "2^63, expected ConversionException"); + // Boundaries still convert + assertEquals(Long.valueOf(Long.MAX_VALUE), converter.convert(clazz, BigInteger.valueOf(Long.MAX_VALUE)), "Maximum"); + assertEquals(Long.valueOf(Long.MIN_VALUE), converter.convert(clazz, BigInteger.valueOf(Long.MIN_VALUE)), "Minimum"); + } + @Test void testSimpleConversion() throws Exception { final String[] message = { "from String", "from String", "from String", "from String", "from String", "from String", "from String", "from Byte", diff --git a/src/test/java/org/apache/commons/beanutils2/converters/LongLocaleConverterTest.java b/src/test/java/org/apache/commons/beanutils2/converters/LongLocaleConverterTest.java index ae15809bf..3ec89c208 100644 --- a/src/test/java/org/apache/commons/beanutils2/converters/LongLocaleConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/converters/LongLocaleConverterTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.math.BigInteger; import java.text.DecimalFormat; import org.apache.commons.beanutils2.ConversionException; @@ -209,6 +210,21 @@ void testLongLimits() { assertThrows(ConversionException.class, () -> converter.convert("-99999999999999999999")); } + /** + * Values just past the long range must not wrap or round into range before the range check sees them: a parsed String one past {@link Long#MAX_VALUE} + * comes back from {@link DecimalFormat} as the {@link Double} 2^63, which a double-based comparison cannot distinguish from {@link Long#MAX_VALUE}, and + * {@code longValue()} of a {@link BigInteger} keeps only the low-order 64 bits, so 2^63 becomes {@link Long#MIN_VALUE}. + */ + @Test + void testOutOfRangeBoundaryRejected() { + converter = LongLocaleConverter.builder().setLocale(defaultLocale).get(); + assertThrows(ConversionException.class, () -> converter.convert("9223372036854775808"), "One more than maximum, expected ConversionException"); + final BigInteger maxPlusOne = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE); + assertThrows(ConversionException.class, () -> converter.convert(maxPlusOne), "2^63, expected ConversionException"); + assertThrows(ConversionException.class, () -> converter.convert(BigInteger.valueOf(Long.MIN_VALUE).subtract(BigInteger.ONE)), + "One less than minimum, expected ConversionException"); + } + /** * Tests that a non-integer value is rejected rather than silently truncated to an integer. */ diff --git a/src/test/java/org/apache/commons/beanutils2/converters/ShortConverterTest.java b/src/test/java/org/apache/commons/beanutils2/converters/ShortConverterTest.java index b368532e8..f3e977277 100644 --- a/src/test/java/org/apache/commons/beanutils2/converters/ShortConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/converters/ShortConverterTest.java @@ -20,6 +20,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.math.BigInteger; + import org.apache.commons.beanutils2.ConversionException; import org.apache.commons.beanutils2.Converter; import org.junit.jupiter.api.AfterEach; @@ -83,6 +85,19 @@ void testInvalidAmount() { assertThrows(ConversionException.class, () -> converter.convert(clazz, maxPlusOne), "More than maximum, expected ConversionException"); } + /** + * A {@link BigInteger} beyond long range wraps to its low-order 64 bits in {@code longValue()}, so it can slip through a long-based bounds check and + * convert to an unrelated in-range value (2^64 + 5 converted to 5); it must be rejected. + */ + @Test + void testWrappedAmount() { + final Converter converter = makeConverter(); + final Class clazz = Short.class; + final BigInteger wrapped = BigInteger.ONE.shiftLeft(64).add(BigInteger.valueOf(5)); + assertThrows(ConversionException.class, () -> converter.convert(clazz, wrapped), "2^64 + 5, expected ConversionException"); + assertThrows(ConversionException.class, () -> converter.convert(clazz, wrapped.negate()), "-(2^64 + 5), expected ConversionException"); + } + @Test void testSimpleConversion() throws Exception { final String[] message = { "from String", "from String", "from String", "from String", "from String", "from String", "from String", "from Byte", diff --git a/src/test/java/org/apache/commons/beanutils2/converters/ShortLocaleConverterTest.java b/src/test/java/org/apache/commons/beanutils2/converters/ShortLocaleConverterTest.java index 76b6834cb..7440fd271 100644 --- a/src/test/java/org/apache/commons/beanutils2/converters/ShortLocaleConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/converters/ShortLocaleConverterTest.java @@ -17,6 +17,11 @@ package org.apache.commons.beanutils2.converters; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.math.BigInteger; + +import org.apache.commons.beanutils2.ConversionException; import org.apache.commons.beanutils2.locale.converters.ShortLocaleConverter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -198,4 +203,15 @@ void testNonIntegerRejected() { converter = ShortLocaleConverter.builder().setDefault(defaultValue).setLocale(defaultLocale).get(); convertValueNoPattern(converter, "non-integer", "5.5", defaultValue); } + + /** + * A {@link BigInteger} beyond long range wraps to its low-order 64 bits in {@code longValue()}, so it can slip through a long-based range check and + * convert to an unrelated in-range value (2^64 + 5 converted to 5); it must be rejected. + */ + @Test + void testWrappedOutOfRangeRejected() { + converter = ShortLocaleConverter.builder().setLocale(defaultLocale).get(); + final BigInteger wrapped = BigInteger.ONE.shiftLeft(64).add(BigInteger.valueOf(5)); + assertThrows(ConversionException.class, () -> converter.convert(wrapped), "2^64 + 5, expected ConversionException"); + } }