From f511bcd022acb3d33fbe8cbf4c44d3de340e4b91 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Thu, 7 Sep 2023 17:37:31 -0700 Subject: [PATCH] Add "max-doc-len" limit, enforcement (#1101) --- README.md | 5 +- release-notes/VERSION-2.x | 2 + .../jackson/core/StreamReadConstraints.java | 117 ++++++++++++++++-- .../core/json/ReaderBasedJsonParser.java | 10 +- .../core/json/UTF8StreamJsonParser.java | 11 +- .../NonBlockingByteBufferJsonParser.java | 7 +- .../json/async/NonBlockingJsonParser.java | 3 + .../core/constraints/LargeDocReadTest.java | 107 ++++++++++++++++ .../StreamReadConstraintsDefaultsTest.java | 8 +- .../core/io/BufferRecyclerPoolTest.java | 6 - .../AsyncReaderWrapperForByteArray.java | 2 +- 11 files changed, 249 insertions(+), 29 deletions(-) create mode 100644 src/test/java/com/fasterxml/jackson/core/constraints/LargeDocReadTest.java diff --git a/README.md b/README.md index cd466add9e..5d687ab4b4 100644 --- a/README.md +++ b/README.md @@ -117,12 +117,14 @@ Implemented limits are: ### Input parsing limits -* Maximum number token length (2.15+): (see https://github.com/FasterXML/jackson-core/issues/815) +* Maximum Number token length (2.15+): (see https://github.com/FasterXML/jackson-core/issues/815) * Default: Maximum 1000 for both integral and floating-point numbers. * Maximum String value length (2.15+): (see https://github.com/FasterXML/jackson-core/issues/863) * Default: 20_000_000 (20 million) (since 2.15.1; 2.15.0 had lower limit, 5 million) * Maximum Input nesting depth (2.15+): (see https://github.com/FasterXML/jackson-core/pull/943) * Default: 1000 levels +* Maximum Document length (2.16+): (see https://github.com/FasterXML/jackson-core/issues/1046) + * Default: Unlimited (-1) ### Output generation limits @@ -137,6 +139,7 @@ You can change per-factory limits as follows: ```java JsonFactory f = JsonFactory.builder() + .streamReadConstraints(StreamReadConstraints.builder().maxDocumentLength(10_000_000L).build()) .streamReadConstraints(StreamReadConstraints.builder().maxNumberLength(250).build()) .streamWriteConstraints(StreamWriteConstraints.builder().maxNestingDepth(2000).build()) .build(); diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 8f02554c94..34a60ffa9a 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -31,6 +31,8 @@ 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) +#1046: Add configurable limit for the maximum number of bytes/chars + of content to parse before failing #1047: Add configurable limit for the maximum length of Object property names to parse before failing (default max: 50,000 chars) (contributed by @pjfanning) diff --git a/src/main/java/com/fasterxml/jackson/core/StreamReadConstraints.java b/src/main/java/com/fasterxml/jackson/core/StreamReadConstraints.java index 5bc5d09ce3..496339fd5f 100644 --- a/src/main/java/com/fasterxml/jackson/core/StreamReadConstraints.java +++ b/src/main/java/com/fasterxml/jackson/core/StreamReadConstraints.java @@ -20,6 +20,9 @@ * *
  • Maximum Nesting depth: default 1000 (see {@link #DEFAULT_MAX_DEPTH}) *
  • + *
  • Maximum Document length: default {@code unlimited} (coded as {@code -1}, + * (see {@link #DEFAULT_MAX_DOC_LEN}) + *
  • * * * @since 2.15 @@ -35,7 +38,13 @@ public class StreamReadConstraints public static final int DEFAULT_MAX_DEPTH = 1000; /** - * Default setting for maximum number length: see {@link Builder#maxNumberLength(int)} for details. + * Default setting for maximum document length: + * see {@link Builder#maxDocumentLength} for details. + */ + public static final long DEFAULT_MAX_DOC_LEN = -1L; + + /** + * @since 2.16 */ public static final int DEFAULT_MAX_NUM_LEN = 1000; @@ -64,13 +73,16 @@ public class StreamReadConstraints private static final int MAX_BIGINT_SCALE_MAGNITUDE = 100_000; protected final int _maxNestingDepth; + protected final long _maxDocLen; + protected final int _maxNumLen; protected final int _maxStringLen; protected final int _maxNameLen; private static StreamReadConstraints DEFAULT = - new StreamReadConstraints(DEFAULT_MAX_DEPTH, DEFAULT_MAX_NUM_LEN, - DEFAULT_MAX_STRING_LEN, DEFAULT_MAX_NAME_LEN); + new StreamReadConstraints(DEFAULT_MAX_DEPTH, + DEFAULT_MAX_DOC_LEN, + DEFAULT_MAX_NUM_LEN, DEFAULT_MAX_STRING_LEN, DEFAULT_MAX_NAME_LEN); /** * Override the default StreamReadConstraints. These defaults are only used when {@link JsonFactory} @@ -91,13 +103,15 @@ public class StreamReadConstraints */ public static void overrideDefaultStreamReadConstraints(final StreamReadConstraints streamReadConstraints) { if (streamReadConstraints == null) { - DEFAULT = new StreamReadConstraints(DEFAULT_MAX_DEPTH, DEFAULT_MAX_NUM_LEN, DEFAULT_MAX_STRING_LEN); + DEFAULT = new StreamReadConstraints(DEFAULT_MAX_DEPTH, DEFAULT_MAX_DOC_LEN, + DEFAULT_MAX_NUM_LEN, DEFAULT_MAX_STRING_LEN); } else { DEFAULT = streamReadConstraints; } } public static final class Builder { + private long maxDocLen; private int maxNestingDepth; private int maxNumLen; private int maxStringLen; @@ -120,6 +134,28 @@ public Builder maxNestingDepth(final int maxNestingDepth) { return this; } + /** + * Sets the maximum allowed document length (for positive values over 0) or + * indicate that any length is acceptable ({@code 0} or negative number). + * The length is in input units of the input source, that is, in + * {@code byte}s or {@code char}s. + * + * @param maxDocLen the maximum allowed document if positive number above 0; otherwise + * ({@code 0} or negative number) means "unlimited". + * + * @return this builder + * + * @since 2.16 + */ + public Builder maxDocumentLength(long maxDocLen) { + // Negative values and 0 mean "unlimited", mark with -1L + if (maxDocLen <= 0L) { + maxDocLen = -1L; + } + this.maxDocLen = maxDocLen; + return this; + } + /** * Sets the maximum number length (in chars or bytes, depending on input context). * The default is 1000. @@ -184,11 +220,14 @@ public Builder maxNameLength(final int maxNameLen) { } Builder() { - this(DEFAULT_MAX_DEPTH, DEFAULT_MAX_NUM_LEN, DEFAULT_MAX_STRING_LEN, DEFAULT_MAX_NAME_LEN); + this(DEFAULT_MAX_DEPTH, DEFAULT_MAX_DOC_LEN, + DEFAULT_MAX_NUM_LEN, DEFAULT_MAX_STRING_LEN, DEFAULT_MAX_NAME_LEN); } - Builder(final int maxNestingDepth, final int maxNumLen, final int maxStringLen, final int maxNameLen) { + Builder(final int maxNestingDepth, final long maxDocLen, + final int maxNumLen, final int maxStringLen, final int maxNameLen) { this.maxNestingDepth = maxNestingDepth; + this.maxDocLen = maxDocLen; this.maxNumLen = maxNumLen; this.maxStringLen = maxStringLen; this.maxNameLen = maxNameLen; @@ -196,13 +235,15 @@ public Builder maxNameLength(final int maxNameLen) { Builder(StreamReadConstraints src) { maxNestingDepth = src._maxNestingDepth; + maxDocLen = src._maxDocLen; maxNumLen = src._maxNumLen; maxStringLen = src._maxStringLen; maxNameLen = src._maxNameLen; } public StreamReadConstraints build() { - return new StreamReadConstraints(maxNestingDepth, maxNumLen, maxStringLen, maxNameLen); + return new StreamReadConstraints(maxNestingDepth, maxDocLen, + maxNumLen, maxStringLen, maxNameLen); } } @@ -213,13 +254,19 @@ public StreamReadConstraints build() { */ @Deprecated // since 2.16 - protected StreamReadConstraints(final int maxNestingDepth, final int maxNumLen, final int maxStringLen) { - this(maxNestingDepth, maxNumLen, maxStringLen, DEFAULT_MAX_NAME_LEN); + protected StreamReadConstraints(final int maxNestingDepth, final long maxDocLen, + final int maxNumLen, final int maxStringLen) { + this(maxNestingDepth, DEFAULT_MAX_DOC_LEN, + maxNumLen, maxStringLen, DEFAULT_MAX_NAME_LEN); } - protected StreamReadConstraints(final int maxNestingDepth, final int maxNumLen, - final int maxStringLen, final int maxNameLen) { + /** + * @since 2.16 + */ + protected StreamReadConstraints(final int maxNestingDepth, final long maxDocLen, + final int maxNumLen, final int maxStringLen, final int maxNameLen) { _maxNestingDepth = maxNestingDepth; + _maxDocLen = maxDocLen; _maxNumLen = maxNumLen; _maxStringLen = maxStringLen; _maxNameLen = maxNameLen; @@ -261,6 +308,29 @@ public int getMaxNestingDepth() { return _maxNestingDepth; } + /** + * Accessor for maximum document length. + * see {@link Builder#maxDocumentLength(long)} for details. + * + * @return Maximum allowed depth + */ + public long getMaxDocumentLength() { + return _maxDocLen; + } + + /** + * Convenience method, basically same as: + *
    +     *  getMaxDocumentLength() > 0L
    +     *
    + * + * @return {@code True} if this constraints instance has a limit for maximum + * document length to enforce; {@code false} otherwise. + */ + public boolean hasMaxDocumentLength() { + return _maxDocLen > 0L; + } + /** * Accessor for maximum length of numbers to decode. * see {@link Builder#maxNumberLength(int)} for details. @@ -318,6 +388,31 @@ public void validateNestingDepth(int depth) throws StreamConstraintsException } } + /** + * Convenience method that can be used to verify that the + * document length does not exceed the maximum specified by this + * constraints object (if any): if it does, a + * {@link StreamConstraintsException} + * is thrown. + * + * @param len Current length of processed document content + * + * @throws StreamConstraintsException If length exceeds maximum + * + * @since 2.16 + */ + public void validateDocumentLength(long len) throws StreamConstraintsException + { + if ((len > _maxDocLen) + // Note: -1L used as marker for "unlimited" + && (_maxDocLen > 0L)) { + throw _constructException( + "Document length (%d) exceeds the maximum allowed (%d, from %s)", + len, _maxDocLen, + _constrainRef("getMaxDocumentLength")); + } + } + /* /********************************************************************** /* Convenience methods for validation, token lengths diff --git a/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java index 92907fbd7b..27b5cfec2a 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java @@ -273,11 +273,14 @@ protected void _loadMoreGuaranteed() throws IOException { protected boolean _loadMore() throws IOException { if (_reader != null) { + final int bufSize = _inputEnd; + _currInputProcessed += bufSize; + _currInputRowStart -= bufSize; + // 06-Sep-2023, tatu: [core#1046] Enforce max doc length limit + streamReadConstraints().validateDocumentLength(_currInputProcessed); + int count = _reader.read(_inputBuffer, 0, _inputBuffer.length); if (count > 0) { - final int bufSize = _inputEnd; - _currInputProcessed += bufSize; - _currInputRowStart -= bufSize; // 26-Nov-2015, tatu: Since name-offset requires it too, must offset // this increase to avoid "moving" name-offset, resulting most likely @@ -289,6 +292,7 @@ protected boolean _loadMore() throws IOException return true; } + _inputPtr = _inputEnd = 0; // End of input _closeInput(); // Should never return 0, so let's fail 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 89dd2b361a..4f43c7eeb9 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java @@ -255,12 +255,14 @@ protected final boolean _loadMore() throws IOException return false; } + final int bufSize = _inputEnd; + _currInputProcessed += bufSize; + _currInputRowStart -= bufSize; + // 06-Sep-2023, tatu: [core#1046] Enforce max doc length limit + streamReadConstraints().validateDocumentLength(_currInputProcessed); + int count = _inputStream.read(_inputBuffer, 0, space); if (count > 0) { - final int bufSize = _inputEnd; - - _currInputProcessed += bufSize; - _currInputRowStart -= bufSize; // 26-Nov-2015, tatu: Since name-offset requires it too, must offset // this increase to avoid "moving" name-offset, resulting most likely @@ -272,6 +274,7 @@ protected final boolean _loadMore() throws IOException return true; } + _inputPtr = _inputEnd = 0; // End of input _closeInput(); // Should never return 0, so let's fail diff --git a/src/main/java/com/fasterxml/jackson/core/json/async/NonBlockingByteBufferJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/async/NonBlockingByteBufferJsonParser.java index cc01d9d84a..ca66d78bf6 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/async/NonBlockingByteBufferJsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/json/async/NonBlockingByteBufferJsonParser.java @@ -21,8 +21,8 @@ * @since 2.14 */ public class NonBlockingByteBufferJsonParser - extends NonBlockingUtf8JsonParserBase - implements ByteBufferFeeder + extends NonBlockingUtf8JsonParserBase + implements ByteBufferFeeder { private ByteBuffer _inputBuffer = ByteBuffer.wrap(NO_BYTES); @@ -56,6 +56,9 @@ public void feedInput(final ByteBuffer byteBuffer) throws IOException { // Time to update pointers first _currInputProcessed += _origBufferLen; + // 06-Sep-2023, tatu: [core#1046] Enforce max doc length limit + streamReadConstraints().validateDocumentLength(_currInputProcessed); + // Also need to adjust row start, to work as if it extended into the past wrt new buffer _currInputRowStart = start - (_inputEnd - _currInputRowStart); diff --git a/src/main/java/com/fasterxml/jackson/core/json/async/NonBlockingJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/async/NonBlockingJsonParser.java index 573a48cec7..fcd90b46fc 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/async/NonBlockingJsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/json/async/NonBlockingJsonParser.java @@ -48,6 +48,9 @@ public void feedInput(final byte[] buf, final int start, final int end) throws I // Time to update pointers first _currInputProcessed += _origBufferLen; + // 06-Sep-2023, tatu: [core#1046] Enforce max doc length limit + streamReadConstraints().validateDocumentLength(_currInputProcessed); + // Also need to adjust row start, to work as if it extended into the past wrt new buffer _currInputRowStart = start - (_inputEnd - _currInputRowStart); diff --git a/src/test/java/com/fasterxml/jackson/core/constraints/LargeDocReadTest.java b/src/test/java/com/fasterxml/jackson/core/constraints/LargeDocReadTest.java new file mode 100644 index 0000000000..0208e19578 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/constraints/LargeDocReadTest.java @@ -0,0 +1,107 @@ +package com.fasterxml.jackson.core.constraints; + +import java.io.IOException; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.core.async.AsyncTestBase; +import com.fasterxml.jackson.core.exc.StreamConstraintsException; +import com.fasterxml.jackson.core.testsupport.AsyncReaderWrapper; + +// [core#1047]: Add max-name-length constraints +public class LargeDocReadTest extends AsyncTestBase +{ + private final JsonFactory JSON_F_DEFAULT = newStreamFactory(); + + private final JsonFactory JSON_F_DOC_10K = JsonFactory.builder() + .streamReadConstraints(StreamReadConstraints.builder().maxDocumentLength(10_000L).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(12_000); + try (JsonParser p = createParserUsingStream(JSON_F_DOC_10K, doc, "UTF-8")) { + consumeTokens(p); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException e) { + verifyMaxDocLen(JSON_F_DOC_10K, e); + } + } + + public void testLargeNameWithSmallLimitChars() throws Exception + { + final String doc = generateJSON(12_000); + try (JsonParser p = createParserUsingReader(JSON_F_DOC_10K, doc)) { + consumeTokens(p); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException e) { + verifyMaxDocLen(JSON_F_DOC_10K, e); + } + } + + public void testLargeNameWithSmallLimitAsync() throws Exception + { + final byte[] doc = utf8Bytes(generateJSON(12_000)); + + // first with byte[] backend + try (AsyncReaderWrapper p = asyncForBytes(JSON_F_DOC_10K, 1000, doc, 1)) { + consumeAsync(p); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException e) { + verifyMaxDocLen(JSON_F_DOC_10K, e); + } + + // then with byte buffer + try (AsyncReaderWrapper p = asyncForByteBuffer(JSON_F_DOC_10K, 1000, doc, 1)) { + consumeAsync(p); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException e) { + verifyMaxDocLen(JSON_F_DOC_10K, e); + } + } + + private void consumeTokens(JsonParser p) throws IOException { + while (p.nextToken() != null) { + ; + } + } + + private void consumeAsync(AsyncReaderWrapper w) throws IOException { + while (w.nextToken() != null) { + ; + } + } + + private String generateJSON(final int docLen) { + final StringBuilder sb = new StringBuilder(); + sb.append("["); + + int i = 0; + while (docLen > sb.length()) { + sb.append(++i).append(",\n"); + } + sb.append("true ] "); + return sb.toString(); + } + + private void verifyMaxDocLen(JsonFactory f, StreamConstraintsException e) { + verifyException(e, "Document length"); + verifyException(e, "exceeds the maximum allowed (" + +f.streamReadConstraints().getMaxDocumentLength() + ); + } +} diff --git a/src/test/java/com/fasterxml/jackson/core/constraints/StreamReadConstraintsDefaultsTest.java b/src/test/java/com/fasterxml/jackson/core/constraints/StreamReadConstraintsDefaultsTest.java index 0b9040f5a4..0cad3ec03a 100644 --- a/src/test/java/com/fasterxml/jackson/core/constraints/StreamReadConstraintsDefaultsTest.java +++ b/src/test/java/com/fasterxml/jackson/core/constraints/StreamReadConstraintsDefaultsTest.java @@ -6,24 +6,30 @@ import static org.junit.Assert.assertEquals; -public class StreamReadConstraintsDefaultsTest { +public class StreamReadConstraintsDefaultsTest +{ @Test public void testOverride() { + final long maxDocLen = 10_000_000L; final int numLen = 1234; final int strLen = 12345; final int depth = 123; StreamReadConstraints constraints = StreamReadConstraints.builder() + .maxDocumentLength(maxDocLen) .maxNumberLength(numLen) .maxStringLength(strLen) .maxNestingDepth(depth) .build(); try { StreamReadConstraints.overrideDefaultStreamReadConstraints(constraints); + assertEquals(maxDocLen, StreamReadConstraints.defaults().getMaxDocumentLength()); assertEquals(depth, StreamReadConstraints.defaults().getMaxNestingDepth()); assertEquals(strLen, StreamReadConstraints.defaults().getMaxStringLength()); assertEquals(numLen, StreamReadConstraints.defaults().getMaxNumberLength()); } finally { StreamReadConstraints.overrideDefaultStreamReadConstraints(null); + assertEquals(StreamReadConstraints.DEFAULT_MAX_DOC_LEN, + StreamReadConstraints.defaults().getMaxDocumentLength()); assertEquals(StreamReadConstraints.DEFAULT_MAX_DEPTH, StreamReadConstraints.defaults().getMaxNestingDepth()); assertEquals(StreamReadConstraints.DEFAULT_MAX_STRING_LEN, diff --git a/src/test/java/com/fasterxml/jackson/core/io/BufferRecyclerPoolTest.java b/src/test/java/com/fasterxml/jackson/core/io/BufferRecyclerPoolTest.java index 9132e06e1f..d13018735e 100644 --- a/src/test/java/com/fasterxml/jackson/core/io/BufferRecyclerPoolTest.java +++ b/src/test/java/com/fasterxml/jackson/core/io/BufferRecyclerPoolTest.java @@ -74,11 +74,5 @@ private static class NopOutputStream extends OutputStream { @Override public void write(byte[] b, int offset, int len) throws IOException { size += len; } - - public NopOutputStream reset() { - size = 0; - return this; - } - public int size() { return size; } } } diff --git a/src/test/java/com/fasterxml/jackson/core/testsupport/AsyncReaderWrapperForByteArray.java b/src/test/java/com/fasterxml/jackson/core/testsupport/AsyncReaderWrapperForByteArray.java index fef29395d9..48c02940be 100644 --- a/src/test/java/com/fasterxml/jackson/core/testsupport/AsyncReaderWrapperForByteArray.java +++ b/src/test/java/com/fasterxml/jackson/core/testsupport/AsyncReaderWrapperForByteArray.java @@ -19,7 +19,7 @@ public class AsyncReaderWrapperForByteArray extends AsyncReaderWrapper private int _end; public AsyncReaderWrapperForByteArray(JsonParser sr, int bytesPerCall, - byte[] doc, int padding) + byte[] doc, int padding) { super(sr); _bytesPerFeed = bytesPerCall;