Skip to content

Commit 3ac7b3a

Browse files
committed
Merge branch '2.20' into 2.21
2 parents df6c297 + 6613744 commit 3ac7b3a

4 files changed

Lines changed: 90 additions & 2 deletions

File tree

release-notes/CREDITS-2.x

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,11 @@ Revanth Meesala (@revanthmeesala)
488488
case [GHSA-2c4j-63jj-9fqr]
489489
(2.18.10)
490490
491+
@tinyb0y
492+
* Contributed #1643: Enforce maxNameLength incrementally in ReaderBasedJsonParser
493+
[GHSA-649p-m576-vr99]
494+
(2.18.10)
495+
491496
Yanming Zhou (@quaff)
492497
* Requested #633: Allow skipping `RS` CTRL-CHAR to support JSON Text Sequences
493498
(2.19.0)

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ No changes since 2.19.1
156156
#1642: Fix maxDocumentLength bypass in async parser single-feedInput() case
157157
[GHSA-2c4j-63jj-9fqr]
158158
(fix by Revanth M)
159+
#1643: Enforce maxNameLength incrementally in ReaderBasedJsonParser [GHSA-649p-m576-vr99]
160+
(fix by @tinyb0y)
159161

160162
2.18.9 (07-Jul-2026)
161163

src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1841,6 +1841,15 @@ private String _parseName2(int startPtr, int hash, int endChar) throws IOExcepti
18411841
*/
18421842
char[] outBuf = _textBuffer.getCurrentSegment();
18431843
int outPtr = _textBuffer.getCurrentSegmentSize();
1844+
// 28-Jul-2026, tinyb0y: [core#1643] Track total length accumulated so far so we
1845+
// can validate against `maxNameLength` incrementally, same as byte-based
1846+
// parsers already do via `ParserBase._growNameDecodeBuffer()`. Without this,
1847+
// only the much larger `maxStringLength` bound (enforced inside
1848+
// `TextBuffer.finishCurrentSegment()`) applies until the whole name has
1849+
// already been buffered.
1850+
// Note: only updated when segment gets full (at which point `outPtr` is
1851+
// always exactly `outBuf.length`), to keep the per-character loop tight.
1852+
int totalLen = 0;
18441853

18451854
while (true) {
18461855
if (_inputPtr >= _inputEnd) {
@@ -1872,6 +1881,8 @@ private String _parseName2(int startPtr, int hash, int endChar) throws IOExcepti
18721881

18731882
// Need more room?
18741883
if (outPtr >= outBuf.length) {
1884+
totalLen += outBuf.length;
1885+
_streamReadConstraints.validateNameLength(totalLen);
18751886
outBuf = _textBuffer.finishCurrentSegment();
18761887
outPtr = 0;
18771888
}
@@ -2102,6 +2113,9 @@ private String _handleOddName2(int startPtr, int hash, int[] codes) throws IOExc
21022113
char[] outBuf = _textBuffer.getCurrentSegment();
21032114
int outPtr = _textBuffer.getCurrentSegmentSize();
21042115
final int maxCode = codes.length;
2116+
// 28-Jul-2026, tinyb0y: [core#1643] Same incremental `maxNameLength` check as
2117+
// `_parseName2()` needs to apply to unquoted ("odd") names as well
2118+
int totalLen = 0;
21052119

21062120
while (true) {
21072121
if (_inputPtr >= _inputEnd) {
@@ -2125,6 +2139,8 @@ private String _handleOddName2(int startPtr, int hash, int[] codes) throws IOExc
21252139

21262140
// Need more room?
21272141
if (outPtr >= outBuf.length) {
2142+
totalLen += outBuf.length;
2143+
_streamReadConstraints.validateNameLength(totalLen);
21282144
outBuf = _textBuffer.finishCurrentSegment();
21292145
outPtr = 0;
21302146
}

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

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package com.fasterxml.jackson.core.constraints;
22

33
import java.io.IOException;
4+
import java.util.regex.Matcher;
5+
import java.util.regex.Pattern;
46

57
import org.junit.jupiter.api.Test;
68

79
import com.fasterxml.jackson.core.*;
810
import com.fasterxml.jackson.core.exc.StreamConstraintsException;
11+
import com.fasterxml.jackson.core.json.JsonReadFeature;
912
import com.fasterxml.jackson.core.json.async.NonBlockingJsonParser;
1013

1114
import static org.junit.jupiter.api.Assertions.fail;
@@ -25,6 +28,13 @@ class LargeNameReadTest extends JUnit5TestBase
2528
.maxNameLength(100).build());
2629
}
2730

31+
// Factory that also allows non-standard name flavors ("apostrophe" and unquoted)
32+
private final JsonFactory JSON_F_NAME_100_ODD = JsonFactory.builder()
33+
.streamReadConstraints(StreamReadConstraints.builder().maxNameLength(100).build())
34+
.configure(JsonReadFeature.ALLOW_SINGLE_QUOTES, true)
35+
.configure(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES, true)
36+
.build();
37+
2838
// Test name that is below default max name
2939
@Test
3040
void largeNameBytes() throws Exception {
@@ -76,6 +86,56 @@ private void _testLargeNameWithSmallLimitChars(JsonFactory jf) throws Exception
7686
}
7787
}
7888

89+
// [core#1643]: Reader-backed parser must reject an over-limit name promptly, the
90+
// same way byte-based input already does -- not only once the entire (possibly
91+
// huge) name has already been buffered.
92+
// (note: `String` / `char[]` input is not affected the same way, since the whole
93+
// document is already in memory and gets scanned in-place, without buffering)
94+
@Test
95+
void largeNameWithSmallLimitCharsFailsFast() throws Exception {
96+
// Name much larger than the configured limit: without incremental checking
97+
// the whole name gets buffered (bounded only by much bigger `maxStringLength`)
98+
// before failing, whereas the fix must reject within a segment fill or two.
99+
_testLargeNameFailsFast(JSON_F_NAME_100, "\"");
100+
_testLargeNameFailsFast(JSON_F_NAME_100_B, "\"");
101+
}
102+
103+
// [core#1643]: ... and same goes for the non-standard name flavors, which are
104+
// decoded by different code paths ("apostrophe" and unquoted names)
105+
@Test
106+
void largeOddNameWithSmallLimitCharsFailsFast() throws Exception {
107+
_testLargeNameFailsFast(JSON_F_NAME_100_ODD, "'");
108+
_testLargeNameFailsFast(JSON_F_NAME_100_ODD, "");
109+
}
110+
111+
private void _testLargeNameFailsFast(JsonFactory jf, String nameQuote) throws Exception
112+
{
113+
final int nameLen = 1_000_000;
114+
final String doc = generateJSON(nameLen, nameQuote);
115+
try (JsonParser p = createParserUsingReader(jf, doc)) {
116+
consumeTokens(p);
117+
fail("expected StreamConstraintsException");
118+
} catch (StreamConstraintsException e) {
119+
verifyException(e, "Name length");
120+
// Length the exception reports tells us how much had been accumulated
121+
// before the check fired: needs to be small fraction of the whole name
122+
final int reportedLen = _reportedNameLength(e);
123+
final int maxExpected = nameLen >> 4;
124+
if (reportedLen > maxExpected) {
125+
fail("Should have failed before buffering "+maxExpected
126+
+" chars (limit is 100), but reported length was: "+reportedLen);
127+
}
128+
}
129+
}
130+
131+
private int _reportedNameLength(StreamConstraintsException e) {
132+
Matcher m = Pattern.compile("Name length \\((\\d+)\\)").matcher(e.getMessage());
133+
if (!m.find()) {
134+
fail("Could not find reported name length from message: "+e.getMessage());
135+
}
136+
return Integer.parseInt(m.group(1));
137+
}
138+
79139
@Test
80140
void largeNameWithSmallLimitAsync() throws Exception
81141
{
@@ -115,12 +175,17 @@ private void consumeTokens(JsonParser p) throws IOException {
115175
}
116176

117177
private String generateJSON(final int nameLen) {
178+
return generateJSON(nameLen, "\"");
179+
}
180+
181+
// @param nameQuote Quote character to use around name; empty String for unquoted name
182+
private String generateJSON(final int nameLen, final String nameQuote) {
118183
final StringBuilder sb = new StringBuilder();
119-
sb.append("{\"");
184+
sb.append("{").append(nameQuote);
120185
for (int i = 0; i < nameLen; i++) {
121186
sb.append("a");
122187
}
123-
sb.append("\":\"value\"}");
188+
sb.append(nameQuote).append(":\"value\"}");
124189
return sb.toString();
125190
}
126191
}

0 commit comments

Comments
 (0)