Skip to content

Commit b0c428e

Browse files
authored
Enforce StreamReadConstraints.maxNumberLength for non-blocking (async) parser (#1555)
1 parent 7c8b6d5 commit b0c428e

File tree

4 files changed

+149
-22
lines changed

4 files changed

+149
-22
lines changed

release-notes/VERSION-2.x

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ a pure JSON library.
2020
(reported by @ventusfortis)
2121
#1548: `StreamReadConstraints.maxDocumentLength` not checked when
2222
creating parser with fixed buffer
23+
#1555: Enforce `StreamReadConstraints.maxNumberLength` for
24+
non-blocking (async) parser
25+
(fix by @pjfanning)
2326

2427
2.18.5 (27-Oct-2025)
2528
(same as 2.18.4.1 on 10-June-2025)

src/main/java/com/fasterxml/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.fasterxml.jackson.core.JsonParseException;
44
import com.fasterxml.jackson.core.JsonToken;
5+
import com.fasterxml.jackson.core.exc.StreamConstraintsException;
56
import com.fasterxml.jackson.core.io.CharTypes;
67
import com.fasterxml.jackson.core.io.IOContext;
78
import com.fasterxml.jackson.core.json.JsonReadFeature;
@@ -363,7 +364,7 @@ protected final JsonToken _finishTokenWithEOF() throws IOException
363364
if (_numberNegative) {
364365
--len;
365366
}
366-
_intLength = len;
367+
_setIntLength(len);
367368
}
368369
return _valueComplete(JsonToken.VALUE_NUMBER_INT);
369370

@@ -1300,15 +1301,15 @@ protected JsonToken _startPositiveNumber(int ch) throws IOException
13001301
while (true) {
13011302
if (ch < INT_0) {
13021303
if (ch == INT_PERIOD) {
1303-
_intLength = outPtr;
1304+
_setIntLength(outPtr);
13041305
++_inputPtr;
13051306
return _startFloat(outBuf, outPtr, ch);
13061307
}
13071308
break;
13081309
}
13091310
if (ch > INT_9) {
13101311
if ((ch | 0x20) == INT_e) { // ~ 'eE'
1311-
_intLength = outPtr;
1312+
_setIntLength(outPtr);
13121313
++_inputPtr;
13131314
return _startFloat(outBuf, outPtr, ch);
13141315
}
@@ -1327,7 +1328,7 @@ protected JsonToken _startPositiveNumber(int ch) throws IOException
13271328
}
13281329
ch = getByteFromBuffer(_inputPtr) & 0xFF;
13291330
}
1330-
_intLength = outPtr;
1331+
_setIntLength(outPtr);
13311332
_textBuffer.setCurrentLength(outPtr);
13321333
return _valueComplete(JsonToken.VALUE_NUMBER_INT);
13331334
}
@@ -1367,15 +1368,15 @@ protected JsonToken _startNegativeNumber() throws IOException
13671368
while (true) {
13681369
if (ch < INT_0) {
13691370
if (ch == INT_PERIOD) {
1370-
_intLength = outPtr-1;
1371+
_setIntLength(outPtr-1);
13711372
++_inputPtr;
13721373
return _startFloat(outBuf, outPtr, ch);
13731374
}
13741375
break;
13751376
}
13761377
if (ch > INT_9) {
13771378
if ((ch | 0x20) == INT_e) { // ~ 'eE'
1378-
_intLength = outPtr-1;
1379+
_setIntLength(outPtr-1);
13791380
++_inputPtr;
13801381
return _startFloat(outBuf, outPtr, ch);
13811382
}
@@ -1393,7 +1394,7 @@ protected JsonToken _startNegativeNumber() throws IOException
13931394
}
13941395
ch = getByteFromBuffer(_inputPtr) & 0xFF;
13951396
}
1396-
_intLength = outPtr-1;
1397+
_setIntLength(outPtr-1);
13971398
_textBuffer.setCurrentLength(outPtr);
13981399
return _valueComplete(JsonToken.VALUE_NUMBER_INT);
13991400
}
@@ -1439,15 +1440,15 @@ protected JsonToken _startPositiveNumber() throws IOException
14391440
while (true) {
14401441
if (ch < INT_0) {
14411442
if (ch == INT_PERIOD) {
1442-
_intLength = outPtr-1;
1443+
_setIntLength(outPtr-1);
14431444
++_inputPtr;
14441445
return _startFloat(outBuf, outPtr, ch);
14451446
}
14461447
break;
14471448
}
14481449
if (ch > INT_9) {
14491450
if ((ch | 0x20) == INT_e) { // ~ 'eE'
1450-
_intLength = outPtr-1;
1451+
_setIntLength(outPtr-1);
14511452
++_inputPtr;
14521453
return _startFloat(outBuf, outPtr, ch);
14531454
}
@@ -1465,7 +1466,7 @@ protected JsonToken _startPositiveNumber() throws IOException
14651466
}
14661467
ch = getByteFromBuffer(_inputPtr) & 0xFF;
14671468
}
1468-
_intLength = outPtr-1;
1469+
_setIntLength(outPtr-1);
14691470
_textBuffer.setCurrentLength(outPtr);
14701471
return _valueComplete(JsonToken.VALUE_NUMBER_INT);
14711472
}
@@ -1697,15 +1698,15 @@ protected JsonToken _finishNumberIntegralPart(char[] outBuf, int outPtr) throws
16971698
int ch = getByteFromBuffer(_inputPtr) & 0xFF;
16981699
if (ch < INT_0) {
16991700
if (ch == INT_PERIOD) {
1700-
_intLength = outPtr+negMod;
1701+
_setIntLength(outPtr+negMod);
17011702
++_inputPtr;
17021703
return _startFloat(outBuf, outPtr, ch);
17031704
}
17041705
break;
17051706
}
17061707
if (ch > INT_9) {
17071708
if ((ch | 0x20) == INT_e) { // ~ 'eE'
1708-
_intLength = outPtr+negMod;
1709+
_setIntLength(outPtr+negMod);
17091710
++_inputPtr;
17101711
return _startFloat(outBuf, outPtr, ch);
17111712
}
@@ -1719,7 +1720,7 @@ protected JsonToken _finishNumberIntegralPart(char[] outBuf, int outPtr) throws
17191720
}
17201721
outBuf[outPtr++] = (char) ch;
17211722
}
1722-
_intLength = outPtr+negMod;
1723+
_setIntLength(outPtr+negMod);
17231724
_textBuffer.setCurrentLength(outPtr);
17241725
return _valueComplete(JsonToken.VALUE_NUMBER_INT);
17251726
}
@@ -1736,7 +1737,7 @@ protected JsonToken _startFloat(char[] outBuf, int outPtr, int ch) throws IOExce
17361737
if (_inputPtr >= _inputEnd) {
17371738
_textBuffer.setCurrentLength(outPtr);
17381739
_minorState = MINOR_NUMBER_FRACTION_DIGITS;
1739-
_fractLength = fractLen;
1740+
_setFractLength(fractLen);
17401741
return _updateTokenToNA();
17411742
}
17421743
ch = getNextSignedByteFromBuffer(); // ok to have sign extension for now
@@ -1757,7 +1758,7 @@ protected JsonToken _startFloat(char[] outBuf, int outPtr, int ch) throws IOExce
17571758
++fractLen;
17581759
}
17591760
}
1760-
_fractLength = fractLen;
1761+
_setFractLength(fractLen);
17611762
int expLen = 0;
17621763
if ((ch | 0x20) == INT_e) { // ~ 'eE' exponent?
17631764
if (outPtr >= outBuf.length) {
@@ -1793,7 +1794,7 @@ protected JsonToken _startFloat(char[] outBuf, int outPtr, int ch) throws IOExce
17931794
if (_inputPtr >= _inputEnd) {
17941795
_textBuffer.setCurrentLength(outPtr);
17951796
_minorState = MINOR_NUMBER_EXPONENT_DIGITS;
1796-
_expLength = expLen;
1797+
_setExpLength(expLen);
17971798
return _updateTokenToNA();
17981799
}
17991800
ch = getNextSignedByteFromBuffer();
@@ -1808,7 +1809,7 @@ protected JsonToken _startFloat(char[] outBuf, int outPtr, int ch) throws IOExce
18081809
--_inputPtr;
18091810
_textBuffer.setCurrentLength(outPtr);
18101811
// negative, int-length, fract-length already set, so...
1811-
_expLength = expLen;
1812+
_setExpLength(expLen);
18121813
return _valueComplete(JsonToken.VALUE_NUMBER_FLOAT);
18131814
}
18141815

@@ -1830,7 +1831,7 @@ protected JsonToken _finishFloatFraction() throws IOException
18301831
outBuf[outPtr++] = (char) ch;
18311832
if (_inputPtr >= _inputEnd) {
18321833
_textBuffer.setCurrentLength(outPtr);
1833-
_fractLength = fractLen;
1834+
_setFractLength(fractLen);
18341835
return JsonToken.NOT_AVAILABLE;
18351836
}
18361837
ch = getNextSignedByteFromBuffer();
@@ -1850,7 +1851,7 @@ protected JsonToken _finishFloatFraction() throws IOException
18501851
_reportUnexpectedNumberChar(ch, "Decimal point not followed by a digit");
18511852
}
18521853
}
1853-
_fractLength = fractLen;
1854+
_setFractLength(fractLen);
18541855
_textBuffer.setCurrentLength(outPtr);
18551856

18561857
// Ok: end of floating point number or exponent?
@@ -1900,7 +1901,7 @@ protected JsonToken _finishFloatExponent(boolean checkSign, int ch) throws IOExc
19001901
outBuf[outPtr++] = (char) ch;
19011902
if (_inputPtr >= _inputEnd) {
19021903
_textBuffer.setCurrentLength(outPtr);
1903-
_expLength = expLen;
1904+
_setExpLength(expLen);
19041905
return JsonToken.NOT_AVAILABLE;
19051906
}
19061907
ch = getNextSignedByteFromBuffer();
@@ -1914,7 +1915,7 @@ protected JsonToken _finishFloatExponent(boolean checkSign, int ch) throws IOExc
19141915
--_inputPtr;
19151916
_textBuffer.setCurrentLength(outPtr);
19161917
// negative, int-length, fract-length already set, so...
1917-
_expLength = expLen;
1918+
_setExpLength(expLen);
19181919
return _valueComplete(JsonToken.VALUE_NUMBER_FLOAT);
19191920
}
19201921

@@ -2998,4 +2999,21 @@ private final int _decodeUTF8_4(int c, int d, int e, int f) throws IOException
29982999
/* Internal methods, other
29993000
/**********************************************************************
30003001
*/
3002+
3003+
private void _setIntLength(final int len) throws StreamConstraintsException {
3004+
_streamReadConstraints.validateIntegerLength(len);
3005+
_intLength = len;
3006+
}
3007+
3008+
private void _setFractLength(final int len) throws StreamConstraintsException {
3009+
// assumes that the _intLength has been updated first
3010+
_streamReadConstraints.validateFPLength(_intLength + len);
3011+
_fractLength = len;
3012+
}
3013+
3014+
private void _setExpLength(final int len) throws StreamConstraintsException {
3015+
// assumes that the _intLength and _fractLength have been updated already
3016+
_streamReadConstraints.validateFPLength(_intLength + _fractLength + len);
3017+
_expLength = len;
3018+
}
30013019
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.fasterxml.jackson.core.constraints;
2+
3+
import com.fasterxml.jackson.core.async.ByteArrayFeeder;
4+
import org.junit.jupiter.api.Test;
5+
6+
import com.fasterxml.jackson.core.*;
7+
import com.fasterxml.jackson.core.exc.StreamConstraintsException;
8+
import com.fasterxml.jackson.core.JsonFactory;
9+
10+
import static org.junit.jupiter.api.Assertions.*;
11+
12+
/**
13+
* Number Length Constraint Bypass in Non-Blocking (Async) JSON Parsers
14+
*/
15+
class AsyncLargeNumberReadTest
16+
extends com.fasterxml.jackson.core.JUnit5TestBase
17+
{
18+
private static final int TEST_NUMBER_LENGTH = StreamReadConstraints.DEFAULT_MAX_NUM_LEN * 2;
19+
20+
private final JsonFactory JSON_F = newStreamFactory();
21+
22+
@Test
23+
void asyncParserFailsTooLongInt() throws Exception {
24+
byte[] payload = buildPayloadWithLongInteger(TEST_NUMBER_LENGTH);
25+
26+
try (JsonParser p = JSON_F.createNonBlockingByteArrayParser()) {
27+
ByteArrayFeeder byteArrayFeeder = (ByteArrayFeeder) p;
28+
byteArrayFeeder.feedInput(payload, 0, payload.length);
29+
byteArrayFeeder.endOfInput();
30+
31+
_asyncParserFailsTooLongNumber(p, JsonToken.VALUE_NUMBER_INT);
32+
}
33+
}
34+
35+
@Test
36+
void asyncParserFailsTooLongDecimal() throws Exception {
37+
byte[] payload = buildPayloadWithLongDecimal(TEST_NUMBER_LENGTH);
38+
39+
try (JsonParser p = JSON_F.createNonBlockingByteArrayParser()) {
40+
ByteArrayFeeder byteArrayFeeder = (ByteArrayFeeder) p;
41+
byteArrayFeeder.feedInput(payload, 0, payload.length);
42+
byteArrayFeeder.endOfInput();
43+
44+
_asyncParserFailsTooLongNumber(p, JsonToken.VALUE_NUMBER_FLOAT);
45+
}
46+
}
47+
48+
@Test
49+
void asyncParserFailsTooLongDecimalWithExponent() throws Exception {
50+
byte[] payload = buildPayloadWithLongExponent(TEST_NUMBER_LENGTH);
51+
52+
try (JsonParser p = JSON_F.createNonBlockingByteArrayParser()) {
53+
ByteArrayFeeder byteArrayFeeder = (ByteArrayFeeder) p;
54+
byteArrayFeeder.feedInput(payload, 0, payload.length);
55+
byteArrayFeeder.endOfInput();
56+
57+
_asyncParserFailsTooLongNumber(p, JsonToken.VALUE_NUMBER_FLOAT);
58+
}
59+
}
60+
61+
private void _asyncParserFailsTooLongNumber(JsonParser p, JsonToken tokenMatch) throws Exception {
62+
boolean foundNumber = false;
63+
try {
64+
while (p.nextToken() != null) {
65+
if (p.currentToken() == tokenMatch) {
66+
foundNumber = true;
67+
String numberText = p.getText();
68+
assertEquals(TEST_NUMBER_LENGTH, numberText.length(),
69+
"Async parser silently accepted all " + TEST_NUMBER_LENGTH + " digits");
70+
}
71+
}
72+
fail("Async parser must reject a " + TEST_NUMBER_LENGTH + "-digit number (number found? "+foundNumber+")");
73+
} catch (StreamConstraintsException e) {
74+
verifyException(e, "Number value length (");
75+
verifyException(e, " exceeds the maximum allowed");
76+
}
77+
}
78+
79+
private byte[] buildPayloadWithLongInteger(int numDigits) {
80+
StringBuilder sb = new StringBuilder(numDigits + 10);
81+
sb.append("{\"v\":");
82+
for (int i = 0; i < numDigits; i++) {
83+
sb.append((char) ('1' + (i % 9)));
84+
}
85+
sb.append('}');
86+
return utf8Bytes(sb.toString());
87+
}
88+
89+
private byte[] buildPayloadWithLongDecimal(int numDigits) {
90+
StringBuilder sb = new StringBuilder(numDigits + 10);
91+
sb.append("{\"v\":0.");
92+
for (int i = 0; i < numDigits; i++) {
93+
sb.append((char) ('1' + (i % 9)));
94+
}
95+
sb.append('}');
96+
return utf8Bytes(sb.toString());
97+
}
98+
99+
private byte[] buildPayloadWithLongExponent(int numDigits) {
100+
StringBuilder sb = new StringBuilder(numDigits + 10);
101+
sb.append("{\"v\":1.1E");
102+
for (int i = 0; i < numDigits; i++) {
103+
sb.append((char) ('1' + (i % 9)));
104+
}
105+
return utf8Bytes(sb.toString());
106+
}
107+
}

src/test/java/com/fasterxml/jackson/core/constraints/LargeNumberReadTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
* Set of basic unit tests for verifying that "too big" number constraints
1515
* are caught by various JSON parser backends.
1616
*/
17-
1817
@SuppressWarnings("resource")
1918
class LargeNumberReadTest
2019
extends com.fasterxml.jackson.core.JUnit5TestBase

0 commit comments

Comments
 (0)