Skip to content

Commit b45bc44

Browse files
Validate number length when coercing StringNode to number (#6232)
1 parent 9f46a7f commit b45bc44

4 files changed

Lines changed: 147 additions & 0 deletions

File tree

‎release-notes/CREDITS‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,9 @@ Aysha Afrah Ziya (@aysha-afrah26)
405405
* Fixed #6179: Apply `StreamReadConstraints` number length limit when coercing
406406
String to `double`
407407
[3.1.7]
408+
* Fixed #6232: Validate `StreamReadConstraints` number length when coercing
409+
`StringNode` to number
410+
[3.1.8]
408411

409412
@waydeshi
410413
* Reported #6127: Add `StreamReadConstraints` number len constraint to

‎release-notes/VERSION‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ No changes since 3.1
1414
#6206: Report cycle through `@JsonValue` accessor as `DatabindException`
1515
(instead of `StackOverflowError`)
1616
(fix by @pjfanning, w/ Claude code)
17+
#6232: Validate `StreamReadConstraints` number length when coercing `StringNode`
18+
to number
19+
(fix by Aysha A-Z)
1720
#6236: Retain `JsonFormat.Shape` in `withFormat()` of `Month`, `MonthDay`, `Year`
1821
and `YearMonth` serializers
1922
(fix by @cowtowncoder, w/ Claude code)

‎src/main/java/tools/jackson/databind/node/StringNode.java‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,12 +243,18 @@ public BigInteger asBigInteger() {
243243

244244
@Override
245245
public BigInteger asBigInteger(BigInteger defaultValue) {
246+
if (!_numberLengthInRange()) {
247+
return defaultValue;
248+
}
246249
BigInteger big = _tryParseAsBigInteger();
247250
return (big == null) ? defaultValue : big;
248251
}
249252

250253
@Override
251254
public Optional<BigInteger> asBigIntegerOpt() {
255+
if (!_numberLengthInRange()) {
256+
return Optional.empty();
257+
}
252258
BigInteger big = _tryParseAsBigInteger();
253259
return (big == null) ? Optional.empty() : Optional.of(big);
254260
}
@@ -269,12 +275,18 @@ public float asFloat()
269275
@Override
270276
public float asFloat(float defaultValue)
271277
{
278+
if (!_numberLengthInRange()) {
279+
return defaultValue;
280+
}
272281
Float F = _tryParseAsFloat();
273282
return (F == null) ? defaultValue : F;
274283
}
275284

276285
@Override
277286
public Optional<Float> asFloatOpt() {
287+
if (!_numberLengthInRange()) {
288+
return Optional.empty();
289+
}
278290
Float F = _tryParseAsFloat();
279291
return (F == null) ? Optional.empty() : Optional.of(F);
280292
}
@@ -295,12 +307,18 @@ public double asDouble()
295307
@Override
296308
public double asDouble(double defaultValue)
297309
{
310+
if (!_numberLengthInRange()) {
311+
return defaultValue;
312+
}
298313
Double d = _tryParseAsDouble();
299314
return (d == null) ? defaultValue : d;
300315
}
301316

302317
@Override
303318
public OptionalDouble asDoubleOpt() {
319+
if (!_numberLengthInRange()) {
320+
return OptionalDouble.empty();
321+
}
304322
Double d = _tryParseAsDouble();
305323
return (d == null) ? OptionalDouble.empty() : OptionalDouble.of(d);
306324
}
@@ -319,12 +337,18 @@ public BigDecimal asDecimal() {
319337

320338
@Override
321339
public BigDecimal asDecimal(BigDecimal defaultValue) {
340+
if (!_numberLengthInRange()) {
341+
return defaultValue;
342+
}
322343
BigDecimal dec = _tryParseAsBigDecimal();
323344
return (dec == null) ? defaultValue : dec;
324345
}
325346

326347
@Override
327348
public Optional<BigDecimal> asDecimalOpt() {
349+
if (!_numberLengthInRange()) {
350+
return Optional.empty();
351+
}
328352
BigDecimal dec = _tryParseAsBigDecimal();
329353
return (dec == null) ? Optional.empty() : Optional.of(dec);
330354
}
@@ -362,6 +386,10 @@ protected Long _tryParseAsLong() {
362386

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

374402
protected Float _tryParseAsFloat() {
375403
if (NumberInput.looksLikeValidNumber(_value)) {
404+
StreamReadConstraints.defaults().validateFPLength(_value.length());
376405
try {
377406
return NumberInput.parseFloat(_value, true);
378407
} catch (NumberFormatException e) {
@@ -384,6 +413,7 @@ protected Float _tryParseAsFloat() {
384413

385414
protected Double _tryParseAsDouble() {
386415
if (NumberInput.looksLikeValidNumber(_value)) {
416+
StreamReadConstraints.defaults().validateFPLength(_value.length());
387417
try {
388418
return NumberInput.parseDouble(_value, true);
389419
} catch (NumberFormatException e) {
@@ -395,6 +425,7 @@ protected Double _tryParseAsDouble() {
395425

396426
protected BigDecimal _tryParseAsBigDecimal() {
397427
if (NumberInput.looksLikeValidNumber(_value)) {
428+
StreamReadConstraints.defaults().validateFPLength(_value.length());
398429
try {
399430
return NumberInput.parseBigDecimal(_value, true);
400431
} catch (NumberFormatException e) {
@@ -403,6 +434,16 @@ protected BigDecimal _tryParseAsBigDecimal() {
403434
}
404435
return null;
405436
}
437+
438+
// [databind#6214]-style number-length guard for the lenient default/`Optional`
439+
// accessors: they must return default/empty rather than throw, so they check the
440+
// limit up front (the strict accessors instead go via the `_tryParseAs...` helpers
441+
// above, which surface `StreamConstraintsException`). Both `validateIntegerLength`
442+
// and `validateFPLength` reject lengths past `getMaxNumberLength()`, so a single
443+
// check covers integer and floating-point coercion alike.
444+
private boolean _numberLengthInRange() {
445+
return _value.length() <= StreamReadConstraints.defaults().getMaxNumberLength();
446+
}
406447

407448
/*
408449
/**********************************************************************
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package tools.jackson.databind.node;
2+
3+
import java.math.BigDecimal;
4+
import java.math.BigInteger;
5+
import java.util.Optional;
6+
import java.util.OptionalDouble;
7+
8+
import org.junit.jupiter.api.Test;
9+
10+
import tools.jackson.core.StreamReadConstraints;
11+
import tools.jackson.core.exc.StreamConstraintsException;
12+
13+
import tools.jackson.databind.testutil.DatabindTestUtil;
14+
15+
import static org.junit.jupiter.api.Assertions.*;
16+
17+
/**
18+
* Verifies that coercing a {@link StringNode} holding a "stringified" number to
19+
* {@code BigInteger}/{@code BigDecimal}/{@code double}/{@code float} enforces the
20+
* {@code StreamReadConstraints} number-length limit before the (super-linear) parse,
21+
* the same way deserializers and (for scale) {@link DecimalNode} already do.
22+
*
23+
* @see <a href="https://github.com/FasterXML/jackson-databind/issues/6214">[databind#6214]</a>
24+
*/
25+
public class StringNodeNumberLengthTest extends DatabindTestUtil
26+
{
27+
private final static int MAX_LEN = StreamReadConstraints.defaults().getMaxNumberLength();
28+
29+
// A numeric String comfortably past the default limit (arrives as a JSON String,
30+
// so it is bounded only by max-string-length, not max-number-length)
31+
private final static int OVER_LEN = MAX_LEN + 100;
32+
private final static String OVER_LONG_INT = "9".repeat(OVER_LEN);
33+
private final static String OVER_LONG_DECIMAL = "1." + "9".repeat(OVER_LEN);
34+
35+
@Test
36+
public void strictAccessorsRejectOverLongNumber() throws Exception
37+
{
38+
StringNode intNode = StringNode.valueOf(OVER_LONG_INT);
39+
_verifyGuarded(() -> intNode.asBigInteger());
40+
_verifyGuarded(() -> intNode.asDouble());
41+
_verifyGuarded(() -> intNode.asFloat());
42+
_verifyGuarded(() -> intNode.asDecimal());
43+
44+
StringNode decNode = StringNode.valueOf(OVER_LONG_DECIMAL);
45+
_verifyGuarded(() -> decNode.asDouble());
46+
_verifyGuarded(() -> decNode.asFloat());
47+
_verifyGuarded(() -> decNode.asDecimal());
48+
}
49+
50+
@Test
51+
public void lenientAccessorsReturnDefaultForOverLongNumber() throws Exception
52+
{
53+
StringNode node = StringNode.valueOf(OVER_LONG_INT);
54+
55+
// default/Optional variants must NOT throw (same contract as any other
56+
// non-convertible value): they return the default / empty instead
57+
assertEquals(BigInteger.ONE, node.asBigInteger(BigInteger.ONE));
58+
assertFalse(node.asBigIntegerOpt().isPresent());
59+
60+
assertEquals(-1.0, node.asDouble(-1.0));
61+
assertFalse(node.asDoubleOpt().isPresent());
62+
63+
assertEquals(-1.0f, node.asFloat(-1.0f));
64+
assertFalse(node.asFloatOpt().isPresent());
65+
66+
assertEquals(BigDecimal.ONE, node.asDecimal(BigDecimal.ONE));
67+
assertFalse(node.asDecimalOpt().isPresent());
68+
}
69+
70+
// Values within the limit must still coerce, unchanged
71+
@Test
72+
public void withinLimitStillCoerces() throws Exception
73+
{
74+
StringNode node = StringNode.valueOf("1234");
75+
assertEquals(new BigInteger("1234"), node.asBigInteger());
76+
assertEquals(new BigInteger("1234"), node.asBigInteger(BigInteger.ZERO));
77+
assertEquals(Optional.of(new BigInteger("1234")), node.asBigIntegerOpt());
78+
assertEquals(1234.0, node.asDouble());
79+
assertEquals(OptionalDouble.of(1234.0), node.asDoubleOpt());
80+
assertEquals(1234.0f, node.asFloat());
81+
assertEquals(new BigDecimal("1234"), node.asDecimal());
82+
83+
// length exactly at the limit is still accepted
84+
StringNode atLimit = StringNode.valueOf("9".repeat(MAX_LEN));
85+
assertEquals(new BigInteger("9".repeat(MAX_LEN)), atLimit.asBigInteger());
86+
}
87+
88+
private interface Coercion { Object convert(); }
89+
90+
private void _verifyGuarded(Coercion c) throws Exception
91+
{
92+
try {
93+
c.convert();
94+
fail("Should not pass: number length exceeds the configured maximum");
95+
} catch (StreamConstraintsException e) {
96+
verifyException(e, "Number value length");
97+
verifyException(e, "exceeds the maximum allowed");
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)