diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index ab4565036f..d85ec14d24 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -31,6 +31,9 @@ a pure JSON library. #1041: Start using AssertJ in unit tests #1042: Allow configuring spaces before and/or after the colon in `DefaultPrettyPrinter` (contributed by @digulla) +#1047: Add configurable limit for the maximum length of Object property names + to parse before failing + (contributed by @pjfanning) #1048: Add configurable processing limits for JSON generator (`StreamWriteConstraints`) (contributed by @pjfanning) #1050: Compare `_snapshotInfo` in `Version` diff --git a/src/main/java/com/fasterxml/jackson/core/StreamReadConstraints.java b/src/main/java/com/fasterxml/jackson/core/StreamReadConstraints.java index 6a67decf51..5bc5d09ce3 100644 --- a/src/main/java/com/fasterxml/jackson/core/StreamReadConstraints.java +++ b/src/main/java/com/fasterxml/jackson/core/StreamReadConstraints.java @@ -402,7 +402,7 @@ public void validateNameLength(int length) throws StreamConstraintsException { if (length > _maxNameLen) { throw _constructException( - "Name value length (%d) exceeds the maximum allowed (%d, from %s)", + "Name length (%d) exceeds the maximum allowed (%d, from %s)", length, _maxNameLen, _constrainRef("getMaxNameLength")); } diff --git a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java index 8976086676..5395e880db 100644 --- a/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java +++ b/src/main/java/com/fasterxml/jackson/core/base/ParserBase.java @@ -6,6 +6,7 @@ import java.util.Arrays; import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.core.exc.StreamConstraintsException; import com.fasterxml.jackson.core.io.IOContext; import com.fasterxml.jackson.core.io.ContentReference; import com.fasterxml.jackson.core.io.NumberInput; @@ -1569,6 +1570,16 @@ protected static int[] growArrayBy(int[] arr, int more) throws IllegalArgumentEx return Arrays.copyOf(arr, len); } + /* Helper method to call to expand "quad" buffer for name decoding + * + * @since 2.16 + */ + protected int[] _growNameDecodeBuffer(int[] arr, int more) throws StreamConstraintsException { + // the following check will fail if the array is already bigger than is allowed for names + _streamReadConstraints.validateNameLength(arr.length << 2); + return growArrayBy(arr, more); + } + /* /********************************************************** /* Stuff that was abstract and required before 2.8, but that diff --git a/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java index f1750e07c6..89dd2b361a 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java @@ -1980,7 +1980,7 @@ protected final String parseLongName(int q, final int q2, int q3) throws IOExcep // Nope, no end in sight. Need to grow quad array etc if (qlen >= _quadBuffer.length) { - _quadBuffer = growArrayWithNameLenCheck(_quadBuffer, qlen); + _quadBuffer = _growNameDecodeBuffer(_quadBuffer, qlen); } _quadBuffer[qlen++] = q; q = i; @@ -2014,12 +2014,6 @@ private final String parseName(int q1, int ch, int lastQuadBytes) throws IOExcep return parseEscapedName(_quadBuffer, 0, q1, ch, lastQuadBytes); } - private int[] growArrayWithNameLenCheck(int[] arr, int more) throws StreamConstraintsException { - // the following check will fail if the array is already bigger than is allowed for names - _streamReadConstraints.validateNameLength(arr.length << 2); - return growArrayBy(_quadBuffer, more); - } - private final String parseName(int q1, int q2, int ch, int lastQuadBytes) throws IOException { _quadBuffer[0] = q1; return parseEscapedName(_quadBuffer, 1, q2, ch, lastQuadBytes); @@ -2063,7 +2057,7 @@ protected final String parseEscapedName(int[] quads, int qlen, int currQuad, int // Ok, we'll need room for first byte right away if (currQuadBytes >= 4) { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = currQuad; currQuad = 0; @@ -2079,7 +2073,7 @@ protected final String parseEscapedName(int[] quads, int qlen, int currQuad, int // need room for middle byte? if (currQuadBytes >= 4) { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = currQuad; currQuad = 0; @@ -2098,7 +2092,7 @@ protected final String parseEscapedName(int[] quads, int qlen, int currQuad, int currQuad = (currQuad << 8) | ch; } else { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = currQuad; currQuad = ch; @@ -2114,7 +2108,7 @@ protected final String parseEscapedName(int[] quads, int qlen, int currQuad, int if (currQuadBytes > 0) { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = _padLastQuad(currQuad, currQuadBytes); } @@ -2174,7 +2168,7 @@ protected String _handleOddName(int ch) throws IOException currQuad = (currQuad << 8) | ch; } else { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = currQuad; currQuad = ch; @@ -2194,7 +2188,7 @@ protected String _handleOddName(int ch) throws IOException if (currQuadBytes > 0) { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = currQuad; } @@ -2248,7 +2242,7 @@ protected String _parseAposName() throws IOException // Ok, we'll need room for first byte right away if (currQuadBytes >= 4) { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = currQuad; currQuad = 0; @@ -2264,7 +2258,7 @@ protected String _parseAposName() throws IOException // need room for middle byte? if (currQuadBytes >= 4) { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = currQuad; currQuad = 0; @@ -2283,7 +2277,7 @@ protected String _parseAposName() throws IOException currQuad = (currQuad << 8) | ch; } else { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = currQuad; currQuad = ch; @@ -2299,7 +2293,7 @@ protected String _parseAposName() throws IOException if (currQuadBytes > 0) { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = _padLastQuad(currQuad, currQuadBytes); } @@ -2364,7 +2358,7 @@ private final String findName(int[] quads, int qlen, int lastQuad, int lastQuadB throws JsonParseException, StreamConstraintsException { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = _padLastQuad(lastQuad, lastQuadBytes); String name = _symbols.findName(quads, qlen); diff --git a/src/main/java/com/fasterxml/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java b/src/main/java/com/fasterxml/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java index 4ceee7b603..de2f081380 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java +++ b/src/main/java/com/fasterxml/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.core.exc.StreamConstraintsException; import com.fasterxml.jackson.core.io.CharTypes; import com.fasterxml.jackson.core.io.IOContext; import com.fasterxml.jackson.core.json.JsonReadFeature; @@ -2097,7 +2096,7 @@ private final JsonToken _parseEscapedName(int qlen, int currQuad, int currQuadBy continue; } if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = currQuad; currQuad = ch; @@ -2130,7 +2129,7 @@ private final JsonToken _parseEscapedName(int qlen, int currQuad, int currQuadBy // 7-bit ASCII. Gets pretty messy. If this happens often, may // want to use different name canonicalization to avoid these hits. if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } if (ch > 127) { // Ok, we'll need room for first byte right away @@ -2170,7 +2169,7 @@ private final JsonToken _parseEscapedName(int qlen, int currQuad, int currQuadBy if (currQuadBytes > 0) { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = _padLastQuad(currQuad, currQuadBytes); } else if (qlen == 0) { // rare, but may happen @@ -2260,7 +2259,7 @@ private JsonToken _finishUnquotedName(int qlen, int currQuad, int currQuadBytes) currQuad = (currQuad << 8) | ch; } else { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = currQuad; currQuad = ch; @@ -2270,7 +2269,7 @@ private JsonToken _finishUnquotedName(int qlen, int currQuad, int currQuadBytes) if (currQuadBytes > 0) { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = currQuad; } @@ -2320,7 +2319,7 @@ private JsonToken _finishAposName(int qlen, int currQuad, int currQuadBytes) // Ok, we'll need room for first byte right away if (currQuadBytes >= 4) { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = currQuad; currQuad = 0; @@ -2336,7 +2335,7 @@ private JsonToken _finishAposName(int qlen, int currQuad, int currQuadBytes) // need room for middle byte? if (currQuadBytes >= 4) { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = currQuad; currQuad = 0; @@ -2355,7 +2354,7 @@ private JsonToken _finishAposName(int qlen, int currQuad, int currQuadBytes) currQuad = (currQuad << 8) | ch; } else { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = currQuad; currQuad = ch; @@ -2365,7 +2364,7 @@ private JsonToken _finishAposName(int qlen, int currQuad, int currQuadBytes) if (currQuadBytes > 0) { if (qlen >= quads.length) { - _quadBuffer = quads = growArrayWithNameLenCheck(quads, quads.length); + _quadBuffer = quads = _growNameDecodeBuffer(quads, quads.length); } quads[qlen++] = _padLastQuad(currQuad, currQuadBytes); } else if (qlen == 0) { // rare case but possible @@ -2387,7 +2386,7 @@ protected final JsonToken _finishFieldWithEscape() throws IOException return JsonToken.NOT_AVAILABLE; } if (_quadLength >= _quadBuffer.length) { - _quadBuffer = growArrayWithNameLenCheck(_quadBuffer, 32); + _quadBuffer = _growNameDecodeBuffer(_quadBuffer, 32); } int currQuad = _pending32; int currQuadBytes = _pendingBytes; @@ -2430,12 +2429,6 @@ protected final JsonToken _finishFieldWithEscape() throws IOException return _parseEscapedName(_quadLength, currQuad, currQuadBytes); } - private int[] growArrayWithNameLenCheck(int[] arr, int more) throws StreamConstraintsException { - // the following check will fail if the array is already bigger than is allowed for names - _streamReadConstraints.validateNameLength(arr.length << 2); - return growArrayBy(_quadBuffer, more); - } - private int _decodeSplitEscaped(int value, int bytesRead) throws IOException { if (_inputPtr >= _inputEnd) { diff --git a/src/main/java/com/fasterxml/jackson/core/sym/CharsToNameCanonicalizer.java b/src/main/java/com/fasterxml/jackson/core/sym/CharsToNameCanonicalizer.java index d5f2f5fbf3..80a1ddce69 100644 --- a/src/main/java/com/fasterxml/jackson/core/sym/CharsToNameCanonicalizer.java +++ b/src/main/java/com/fasterxml/jackson/core/sym/CharsToNameCanonicalizer.java @@ -499,8 +499,8 @@ public String findSymbol(char[] buffer, int start, int len, int h) throws IOExce if (len < 1) { // empty Strings are simplest to handle up front return ""; } - _streamReadConstraints.validateNameLength(len); if (!_canonicalize) { // [JACKSON-259] + _streamReadConstraints.validateNameLength(len); return new String(buffer, start, len); } @@ -536,6 +536,7 @@ public String findSymbol(char[] buffer, int start, int len, int h) throws IOExce } } } + _streamReadConstraints.validateNameLength(len); return _addSymbol(buffer, start, len, h, index); } diff --git a/src/test/java/com/fasterxml/jackson/core/constraints/LargeNameReadTest.java b/src/test/java/com/fasterxml/jackson/core/constraints/LargeNameReadTest.java new file mode 100644 index 0000000000..8d8f536206 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/constraints/LargeNameReadTest.java @@ -0,0 +1,84 @@ +package com.fasterxml.jackson.core.constraints; + +import java.io.IOException; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.core.StreamReadConstraints; +import com.fasterxml.jackson.core.exc.StreamConstraintsException; +import com.fasterxml.jackson.core.json.async.NonBlockingJsonParser; + +// [core#1047]: Add max-name-length constraints +public class LargeNameReadTest extends BaseTest +{ + private final JsonFactory JSON_F_DEFAULT = newStreamFactory(); + + private final JsonFactory JSON_F_NAME_100 = JsonFactory.builder() + .streamReadConstraints(StreamReadConstraints.builder().maxNameLength(100).build()) + .build(); + + // Test name that is below default max name + public void testLargeNameBytes() throws Exception { + final String doc = generateJSON(StreamReadConstraints.defaults().getMaxNameLength() - 100); + try (JsonParser p = createParserUsingStream(JSON_F_DEFAULT, doc, "UTF-8")) { + consumeTokens(p); + } + } + + public void testLargeNameChars() throws Exception { + final String doc = generateJSON(StreamReadConstraints.defaults().getMaxNameLength() - 100); + try (JsonParser p = createParserUsingReader(JSON_F_DEFAULT, doc)) { + consumeTokens(p); + } + } + + public void testLargeNameWithSmallLimitBytes() throws Exception + { + final String doc = generateJSON(1000); + try (JsonParser p = createParserUsingStream(JSON_F_NAME_100, doc, "UTF-8")) { + consumeTokens(p); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException e) { + verifyException(e, "Name length"); + } + } + + public void testLargeNameWithSmallLimitCharss() throws Exception + { + final String doc = generateJSON(1000); + try (JsonParser p = createParserUsingReader(JSON_F_NAME_100, doc)) { + consumeTokens(p); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException e) { + verifyException(e, "Name length"); + } + } + + public void testLargeNameWithSmallLimitAsync() throws Exception + { + final byte[] doc = utf8Bytes(generateJSON(1000)); + + try (NonBlockingJsonParser p = (NonBlockingJsonParser) JSON_F_NAME_100.createNonBlockingByteArrayParser()) { + p.feedInput(doc, 0, doc.length); + consumeTokens(p); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException e) { + verifyException(e, "Name length"); + } + } + + private void consumeTokens(JsonParser p) throws IOException { + while (p.nextToken() != null) { + ; + } + } + + private String generateJSON(final int nameLen) { + final StringBuilder sb = new StringBuilder(); + sb.append("{\""); + for (int i = 0; i < nameLen; i++) { + sb.append("a"); + } + sb.append("\":\"value\"}"); + return sb.toString(); + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/read/LargeNameReadTest.java b/src/test/java/com/fasterxml/jackson/core/read/LargeNameReadTest.java deleted file mode 100644 index 14c6e5d544..0000000000 --- a/src/test/java/com/fasterxml/jackson/core/read/LargeNameReadTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.fasterxml.jackson.core.read; - -import com.fasterxml.jackson.core.BaseTest; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.core.StreamReadConstraints; -import com.fasterxml.jackson.core.exc.StreamConstraintsException; -import com.fasterxml.jackson.core.json.async.NonBlockingJsonParser; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -public class LargeNameReadTest extends BaseTest { - - public void testLargeName() throws Exception - { - final String doc = generateJSON(1000); - final JsonFactory jsonFactory = JsonFactory.builder().build(); - try (JsonParser jp = createParserUsingStream(jsonFactory, doc, "UTF-8")) { - consumeTokens(jp); - } - } - - public void testLargeNameWithSmallLimit() throws Exception - { - final String doc = generateJSON(1000); - final JsonFactory jsonFactory = JsonFactory.builder() - .streamReadConstraints(StreamReadConstraints.builder().maxNameLength(100).build()) - .build(); - try (JsonParser jp = createParserUsingStream(jsonFactory, doc, "UTF-8")) { - consumeTokens(jp); - fail("expected StreamConstraintsException"); - } catch (StreamConstraintsException e) { - assertTrue("Unexpected exception message: " + e.getMessage(), - e.getMessage().contains("Name value length")); - } - } - - public void testAsyncLargeNameWithSmallLimit() throws Exception - { - final byte[] doc = generateJSON(1000).getBytes(StandardCharsets.UTF_8); - final JsonFactory jsonFactory = JsonFactory.builder() - .streamReadConstraints(StreamReadConstraints.builder().maxNameLength(100).build()) - .build(); - - try (NonBlockingJsonParser jp = (NonBlockingJsonParser) jsonFactory.createNonBlockingByteArrayParser()) { - jp.feedInput(doc, 0, doc.length); - consumeTokens(jp); - fail("expected StreamConstraintsException"); - } catch (StreamConstraintsException e) { - assertTrue("Unexpected exception message: " + e.getMessage(), - e.getMessage().contains("Name value length")); - } - } - - public void testReaderLargeNameWithSmallLimit() throws Exception - { - final String doc = generateJSON(1000); - final JsonFactory jsonFactory = JsonFactory.builder() - .streamReadConstraints(StreamReadConstraints.builder().maxNameLength(100).build()) - .build(); - try (JsonParser jp = createParserUsingReader(jsonFactory, doc)) { - consumeTokens(jp); - fail("expected StreamConstraintsException"); - } catch (StreamConstraintsException e) { - assertTrue("Unexpected exception message: " + e.getMessage(), - e.getMessage().contains("Name value length")); - } - } - - private void consumeTokens(JsonParser jp) throws IOException { - JsonToken jsonToken; - while ((jsonToken = jp.nextToken()) != null) { - - } - } - - private String generateJSON(final int nameLen) { - final StringBuilder sb = new StringBuilder(); - sb.append("{\""); - for (int i = 0; i < nameLen; i++) { - sb.append("a"); - } - sb.append("\":\"value\"}"); - return sb.toString(); - } -}