Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,9 @@ Aysha Afrah Ziya (@aysha-afrah26)
* Fixed #6179: Apply `StreamReadConstraints` number length limit when coercing
String to `double`
[3.1.7]
* Fixed #6232: Validate `StreamReadConstraints` number length when coercing
`StringNode` to number
[3.1.8]

@waydeshi
* Reported #6127: Add `StreamReadConstraints` number len constraint to
Expand Down
3 changes: 3 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ No changes since 3.1
#6206: Report cycle through `@JsonValue` accessor as `DatabindException`
(instead of `StackOverflowError`)
(fix by @pjfanning, w/ Claude code)
#6232: Validate `StreamReadConstraints` number length when coercing `StringNode`
to number
(fix by Aysha A-Z)
#6236: Retain `JsonFormat.Shape` in `withFormat()` of `Month`, `MonthDay`, `Year`
and `YearMonth` serializers
(fix by @cowtowncoder, w/ Claude code)
Expand Down
41 changes: 41 additions & 0 deletions src/main/java/tools/jackson/databind/node/StringNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -243,12 +243,18 @@ public BigInteger asBigInteger() {

@Override
public BigInteger asBigInteger(BigInteger defaultValue) {
if (!_numberLengthInRange()) {
return defaultValue;
}
BigInteger big = _tryParseAsBigInteger();
return (big == null) ? defaultValue : big;
}

@Override
public Optional<BigInteger> asBigIntegerOpt() {
if (!_numberLengthInRange()) {
return Optional.empty();
}
BigInteger big = _tryParseAsBigInteger();
return (big == null) ? Optional.empty() : Optional.of(big);
}
Expand All @@ -269,12 +275,18 @@ public float asFloat()
@Override
public float asFloat(float defaultValue)
{
if (!_numberLengthInRange()) {
return defaultValue;
}
Float F = _tryParseAsFloat();
return (F == null) ? defaultValue : F;
}

@Override
public Optional<Float> asFloatOpt() {
if (!_numberLengthInRange()) {
return Optional.empty();
}
Float F = _tryParseAsFloat();
return (F == null) ? Optional.empty() : Optional.of(F);
}
Expand All @@ -295,12 +307,18 @@ public double asDouble()
@Override
public double asDouble(double defaultValue)
{
if (!_numberLengthInRange()) {
return defaultValue;
}
Double d = _tryParseAsDouble();
return (d == null) ? defaultValue : d;
}

@Override
public OptionalDouble asDoubleOpt() {
if (!_numberLengthInRange()) {
return OptionalDouble.empty();
}
Double d = _tryParseAsDouble();
return (d == null) ? OptionalDouble.empty() : OptionalDouble.of(d);
}
Expand All @@ -319,12 +337,18 @@ public BigDecimal asDecimal() {

@Override
public BigDecimal asDecimal(BigDecimal defaultValue) {
if (!_numberLengthInRange()) {
return defaultValue;
}
BigDecimal dec = _tryParseAsBigDecimal();
return (dec == null) ? defaultValue : dec;
}

@Override
public Optional<BigDecimal> asDecimalOpt() {
if (!_numberLengthInRange()) {
return Optional.empty();
}
BigDecimal dec = _tryParseAsBigDecimal();
return (dec == null) ? Optional.empty() : Optional.of(dec);
}
Expand Down Expand Up @@ -362,6 +386,10 @@ protected Long _tryParseAsLong() {

protected BigInteger _tryParseAsBigInteger() {
if (NumberInput.looksLikeValidNumber(_value)) {
// Enforce number-length limit before the super-linear parse, same as
// deserializers do; no `StreamReadConstraints` available here so use
// `defaults()` (compare `DecimalNode`/`POJONode`, [databind#6214])
StreamReadConstraints.defaults().validateIntegerLength(_value.length());
try {
return NumberInput.parseBigInteger(_value, true);
} catch (NumberFormatException e) {
Expand All @@ -373,6 +401,7 @@ protected BigInteger _tryParseAsBigInteger() {

protected Float _tryParseAsFloat() {
if (NumberInput.looksLikeValidNumber(_value)) {
StreamReadConstraints.defaults().validateFPLength(_value.length());
try {
return NumberInput.parseFloat(_value, true);
} catch (NumberFormatException e) {
Expand All @@ -384,6 +413,7 @@ protected Float _tryParseAsFloat() {

protected Double _tryParseAsDouble() {
if (NumberInput.looksLikeValidNumber(_value)) {
StreamReadConstraints.defaults().validateFPLength(_value.length());
try {
return NumberInput.parseDouble(_value, true);
} catch (NumberFormatException e) {
Expand All @@ -395,6 +425,7 @@ protected Double _tryParseAsDouble() {

protected BigDecimal _tryParseAsBigDecimal() {
if (NumberInput.looksLikeValidNumber(_value)) {
StreamReadConstraints.defaults().validateFPLength(_value.length());
try {
return NumberInput.parseBigDecimal(_value, true);
} catch (NumberFormatException e) {
Expand All @@ -403,6 +434,16 @@ protected BigDecimal _tryParseAsBigDecimal() {
}
return null;
}

// [databind#6214]-style number-length guard for the lenient default/`Optional`
// accessors: they must return default/empty rather than throw, so they check the
// limit up front (the strict accessors instead go via the `_tryParseAs...` helpers
// above, which surface `StreamConstraintsException`). Both `validateIntegerLength`
// and `validateFPLength` reject lengths past `getMaxNumberLength()`, so a single
// check covers integer and floating-point coercion alike.
private boolean _numberLengthInRange() {
return _value.length() <= StreamReadConstraints.defaults().getMaxNumberLength();
}

/*
/**********************************************************************
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package tools.jackson.databind.node;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Optional;
import java.util.OptionalDouble;

import org.junit.jupiter.api.Test;

import tools.jackson.core.StreamReadConstraints;
import tools.jackson.core.exc.StreamConstraintsException;

import tools.jackson.databind.testutil.DatabindTestUtil;

import static org.junit.jupiter.api.Assertions.*;

/**
* Verifies that coercing a {@link StringNode} holding a "stringified" number to
* {@code BigInteger}/{@code BigDecimal}/{@code double}/{@code float} enforces the
* {@code StreamReadConstraints} number-length limit before the (super-linear) parse,
* the same way deserializers and (for scale) {@link DecimalNode} already do.
*
* @see <a href="https://github.com/FasterXML/jackson-databind/issues/6214">[databind#6214]</a>
*/
public class StringNodeNumberLengthTest extends DatabindTestUtil
{
private final static int MAX_LEN = StreamReadConstraints.defaults().getMaxNumberLength();

// A numeric String comfortably past the default limit (arrives as a JSON String,
// so it is bounded only by max-string-length, not max-number-length)
private final static int OVER_LEN = MAX_LEN + 100;
private final static String OVER_LONG_INT = "9".repeat(OVER_LEN);
private final static String OVER_LONG_DECIMAL = "1." + "9".repeat(OVER_LEN);

@Test
public void strictAccessorsRejectOverLongNumber() throws Exception
{
StringNode intNode = StringNode.valueOf(OVER_LONG_INT);
_verifyGuarded(() -> intNode.asBigInteger());
_verifyGuarded(() -> intNode.asDouble());
_verifyGuarded(() -> intNode.asFloat());
_verifyGuarded(() -> intNode.asDecimal());

StringNode decNode = StringNode.valueOf(OVER_LONG_DECIMAL);
_verifyGuarded(() -> decNode.asDouble());
_verifyGuarded(() -> decNode.asFloat());
_verifyGuarded(() -> decNode.asDecimal());
}

@Test
public void lenientAccessorsReturnDefaultForOverLongNumber() throws Exception
{
StringNode node = StringNode.valueOf(OVER_LONG_INT);

// default/Optional variants must NOT throw (same contract as any other
// non-convertible value): they return the default / empty instead
assertEquals(BigInteger.ONE, node.asBigInteger(BigInteger.ONE));
assertFalse(node.asBigIntegerOpt().isPresent());

assertEquals(-1.0, node.asDouble(-1.0));
assertFalse(node.asDoubleOpt().isPresent());

assertEquals(-1.0f, node.asFloat(-1.0f));
assertFalse(node.asFloatOpt().isPresent());

assertEquals(BigDecimal.ONE, node.asDecimal(BigDecimal.ONE));
assertFalse(node.asDecimalOpt().isPresent());
}

// Values within the limit must still coerce, unchanged
@Test
public void withinLimitStillCoerces() throws Exception
{
StringNode node = StringNode.valueOf("1234");
assertEquals(new BigInteger("1234"), node.asBigInteger());
assertEquals(new BigInteger("1234"), node.asBigInteger(BigInteger.ZERO));
assertEquals(Optional.of(new BigInteger("1234")), node.asBigIntegerOpt());
assertEquals(1234.0, node.asDouble());
assertEquals(OptionalDouble.of(1234.0), node.asDoubleOpt());
assertEquals(1234.0f, node.asFloat());
assertEquals(new BigDecimal("1234"), node.asDecimal());

// length exactly at the limit is still accepted
StringNode atLimit = StringNode.valueOf("9".repeat(MAX_LEN));
assertEquals(new BigInteger("9".repeat(MAX_LEN)), atLimit.asBigInteger());
}

private interface Coercion { Object convert(); }

private void _verifyGuarded(Coercion c) throws Exception
{
try {
c.convert();
fail("Should not pass: number length exceeds the configured maximum");
} catch (StreamConstraintsException e) {
verifyException(e, "Number value length");
verifyException(e, "exceeds the maximum allowed");
}
}
}
Loading