diff --git a/src/main/java/com/fasterxml/jackson/core/JsonFactory.java b/src/main/java/com/fasterxml/jackson/core/JsonFactory.java index bee48c17d6..b0608b98fa 100644 --- a/src/main/java/com/fasterxml/jackson/core/JsonFactory.java +++ b/src/main/java/com/fasterxml/jackson/core/JsonFactory.java @@ -282,6 +282,14 @@ public static int collectDefaults() { */ protected StreamReadConstraints _streamReadConstraints; + /** + * Write constraints to use for {@link JsonGenerator}s constructed using + * this factory. + * + * @since 2.16 + */ + protected StreamWriteConstraints _streamWriteConstraints; + /** * Optional helper object that may decorate input sources, to do * additional processing on input during parsing. @@ -350,6 +358,7 @@ public JsonFactory(ObjectCodec oc) { _objectCodec = oc; _quoteChar = DEFAULT_QUOTE_CHAR; _streamReadConstraints = StreamReadConstraints.defaults(); + _streamWriteConstraints = StreamWriteConstraints.defaults(); _generatorDecorators = null; } @@ -374,6 +383,8 @@ protected JsonFactory(JsonFactory src, ObjectCodec codec) _generatorDecorators = _copy(src._generatorDecorators); _streamReadConstraints = src._streamReadConstraints == null ? StreamReadConstraints.defaults() : src._streamReadConstraints; + _streamWriteConstraints = src._streamWriteConstraints == null ? + StreamWriteConstraints.defaults() : src._streamWriteConstraints; // JSON-specific _characterEscapes = src._characterEscapes; @@ -401,6 +412,8 @@ public JsonFactory(JsonFactoryBuilder b) { _generatorDecorators = _copy(b._generatorDecorators); _streamReadConstraints = b._streamReadConstraints == null ? StreamReadConstraints.defaults() : b._streamReadConstraints; + _streamWriteConstraints = b._streamWriteConstraints == null ? + StreamWriteConstraints.defaults() : b._streamWriteConstraints; // JSON-specific _characterEscapes = b._characterEscapes; @@ -428,6 +441,8 @@ protected JsonFactory(TSFBuilder b, boolean bogus) { _generatorDecorators = _copy(b._generatorDecorators); _streamReadConstraints = b._streamReadConstraints == null ? StreamReadConstraints.defaults() : b._streamReadConstraints; + _streamWriteConstraints = b._streamWriteConstraints == null ? + StreamWriteConstraints.defaults() : b._streamWriteConstraints; // JSON-specific: need to assign even if not really used _characterEscapes = null; @@ -802,6 +817,11 @@ public StreamReadConstraints streamReadConstraints() { return _streamReadConstraints; } + @Override + public StreamWriteConstraints streamWriteConstraints() { + return _streamWriteConstraints; + } + /** * Method for overriding {@link StreamReadConstraints} defined for * this factory. @@ -822,6 +842,26 @@ public JsonFactory setStreamReadConstraints(StreamReadConstraints src) { return this; } + /** + * Method for overriding {@link StreamWriteConstraints} defined for + * this factory. + *

+ * NOTE: the preferred way to set constraints is by using + * {@link JsonFactoryBuilder#streamWriteConstraints}: this method is only + * provided to support older non-builder-based construction. + * In Jackson 3.x this method will not be available. + * + * @param swc Constraints + * + * @return This factory instance (to allow call chaining) + * + * @since 2.16 + */ + public JsonFactory setStreamWriteConstraints(StreamWriteConstraints swc) { + _streamWriteConstraints = Objects.requireNonNull(swc); + return this; + } + /* /********************************************************** /* Configuration, parser configuration @@ -2076,7 +2116,8 @@ protected IOContext _createContext(ContentReference contentRef, boolean resource if (contentRef == null) { contentRef = ContentReference.unknown(); } - return new IOContext(_streamReadConstraints, _getBufferRecycler(), contentRef, resourceManaged); + return new IOContext(_streamReadConstraints, _streamWriteConstraints, + _getBufferRecycler(), contentRef, resourceManaged); } /** @@ -2091,7 +2132,8 @@ protected IOContext _createContext(ContentReference contentRef, boolean resource */ @Deprecated // @since 2.13 protected IOContext _createContext(Object rawContentRef, boolean resourceManaged) { - return new IOContext(_streamReadConstraints, _getBufferRecycler(), + return new IOContext(_streamReadConstraints, _streamWriteConstraints, + _getBufferRecycler(), _createContentReference(rawContentRef), resourceManaged); } @@ -2109,7 +2151,8 @@ protected IOContext _createContext(Object rawContentRef, boolean resourceManaged protected IOContext _createNonBlockingContext(Object srcRef) { // [jackson-core#479]: allow recycling for non-blocking parser again // now that access is thread-safe - return new IOContext(_streamReadConstraints, _getBufferRecycler(), + return new IOContext(_streamReadConstraints, _streamWriteConstraints, + _getBufferRecycler(), _createContentReference(srcRef), false); } diff --git a/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java b/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java index 8137453ffc..d524939aef 100644 --- a/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java +++ b/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java @@ -341,6 +341,15 @@ protected JsonGenerator() { } */ public abstract ObjectCodec getCodec(); + /** + * Get the constraints to apply when performing streaming writes. + * + * @since 2.16 + */ + public StreamWriteConstraints streamWriteConstraints() { + return StreamWriteConstraints.defaults(); + } + /** * Accessor for finding out version of the bundle that provided this generator instance. * diff --git a/src/main/java/com/fasterxml/jackson/core/StreamWriteConstraints.java b/src/main/java/com/fasterxml/jackson/core/StreamWriteConstraints.java new file mode 100644 index 0000000000..83ac7ca66f --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/core/StreamWriteConstraints.java @@ -0,0 +1,157 @@ +package com.fasterxml.jackson.core; + +import com.fasterxml.jackson.core.exc.StreamConstraintsException; + +/** + * The constraints to use for streaming writes: used to guard against problematic + * output by preventing processing of "too big" output constructs (values, + * structures). + * Constraints are registered with {@code TokenStreamFactory} (such as + * {@code JsonFactory}); if nothing explicitly specified, default + * constraints are used. + *

+ * Currently constrained aspects, with default settings, are: + *

+ * + * @since 2.16 + */ +public class StreamWriteConstraints + implements java.io.Serializable +{ + private static final long serialVersionUID = 1L; + + /** + * Default setting for maximum depth: see {@link Builder#maxNestingDepth(int)} for details. + */ + public static final int DEFAULT_MAX_DEPTH = 1000; + + protected final int _maxNestingDepth; + + private static final StreamWriteConstraints DEFAULT = + new StreamWriteConstraints(DEFAULT_MAX_DEPTH); + + public static final class Builder { + private int maxNestingDepth; + + /** + * Sets the maximum nesting depth. The depth is a count of objects and arrays that have not + * been closed, `{` and `[` respectively. + * + * @param maxNestingDepth the maximum depth + * + * @return this builder + * @throws IllegalArgumentException if the maxNestingDepth is set to a negative value + */ + public Builder maxNestingDepth(final int maxNestingDepth) { + if (maxNestingDepth < 0) { + throw new IllegalArgumentException("Cannot set maxNestingDepth to a negative value"); + } + this.maxNestingDepth = maxNestingDepth; + return this; + } + + Builder() { + this(DEFAULT_MAX_DEPTH); + } + + Builder(final int maxNestingDepth) { + this.maxNestingDepth = maxNestingDepth; + } + + Builder(StreamWriteConstraints src) { + maxNestingDepth = src._maxNestingDepth; + } + + public StreamWriteConstraints build() { + return new StreamWriteConstraints(maxNestingDepth); + } + } + + /* + /********************************************************************** + /* Life-cycle + /********************************************************************** + */ + + protected StreamWriteConstraints(final int maxNestingDepth) { + _maxNestingDepth = maxNestingDepth; + } + + public static Builder builder() { + return new Builder(); + } + + public static StreamWriteConstraints defaults() { + return DEFAULT; + } + + /** + * @return New {@link Builder} initialized with settings of this constraints + * instance + */ + public Builder rebuild() { + return new Builder(this); + } + + /* + /********************************************************************** + /* Accessors + /********************************************************************** + */ + + /** + * Accessor for maximum depth. + * see {@link Builder#maxNestingDepth(int)} for details. + * + * @return Maximum allowed depth + */ + public int getMaxNestingDepth() { + return _maxNestingDepth; + } + + /* + /********************************************************************** + /* Convenience methods for validation, document limits + /********************************************************************** + */ + + /** + * Convenience method that can be used to verify that the + * nesting depth does not exceed the maximum specified by this + * constraints object: if it does, a + * {@link StreamConstraintsException} + * is thrown. + * + * @param depth count of unclosed objects and arrays + * + * @throws StreamConstraintsException If depth exceeds maximum + */ + public void validateNestingDepth(int depth) throws StreamConstraintsException + { + if (depth > _maxNestingDepth) { + throw _constructException( + "Document nesting depth (%d) exceeds the maximum allowed (%d, from %s)", + depth, _maxNestingDepth, + _constrainRef("getMaxNestingDepth")); + } + } + + /* + /********************************************************************** + /* Error reporting + /********************************************************************** + */ + + // @since 2.16 + protected StreamConstraintsException _constructException(String msgTemplate, Object... args) throws StreamConstraintsException { + throw new StreamConstraintsException(String.format(msgTemplate, args)); + } + + // @since 2.16 + protected String _constrainRef(String method) { + return "`StreamWriteConstraints."+method+"()`"; + } +} diff --git a/src/main/java/com/fasterxml/jackson/core/TSFBuilder.java b/src/main/java/com/fasterxml/jackson/core/TSFBuilder.java index f85308a848..fe0965f93c 100644 --- a/src/main/java/com/fasterxml/jackson/core/TSFBuilder.java +++ b/src/main/java/com/fasterxml/jackson/core/TSFBuilder.java @@ -82,12 +82,19 @@ public abstract class TSFBuilderStreamWriteConstraints */ public IOContext(StreamReadConstraints src, BufferRecycler br, ContentReference contentRef, boolean managedResource) { _streamReadConstraints = (src == null) ? StreamReadConstraints.defaults() : src; + _streamWriteConstraints = StreamWriteConstraints.defaults(); _bufferRecycler = br; _contentReference = contentRef; _sourceRef = contentRef.getRawContent(); @@ -141,7 +168,7 @@ public IOContext(StreamReadConstraints src, BufferRecycler br, @Deprecated // since 2.15 public IOContext(BufferRecycler br, ContentReference contentRef, boolean managedResource) { - this(null, br, contentRef, managedResource); + this(null, null, br, contentRef, managedResource); } @Deprecated // since 2.13 @@ -157,6 +184,14 @@ public StreamReadConstraints streamReadConstraints() { return _streamReadConstraints; } + /** + * @return constraints for streaming writes + * @since 2.16 + */ + public StreamWriteConstraints streamWriteConstraints() { + return _streamWriteConstraints; + } + public void setEncoding(JsonEncoding enc) { _encoding = enc; } diff --git a/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java b/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java index 807a92fda8..da3c2de251 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java +++ b/src/main/java/com/fasterxml/jackson/core/json/JsonGeneratorImpl.java @@ -139,6 +139,17 @@ public Version version() { return VersionUtil.versionFor(getClass()); } + /* + /********************************************************************** + /* Constraints violation checking (2.16) + /********************************************************************** + */ + + @Override + public StreamWriteConstraints streamWriteConstraints() { + return _ioContext.streamWriteConstraints(); + } + /* /********************************************************** /* Overridden configuration methods diff --git a/src/main/java/com/fasterxml/jackson/core/json/JsonWriteContext.java b/src/main/java/com/fasterxml/jackson/core/json/JsonWriteContext.java index 87b1ef4ad0..12103e1cc1 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/JsonWriteContext.java +++ b/src/main/java/com/fasterxml/jackson/core/json/JsonWriteContext.java @@ -69,6 +69,7 @@ protected JsonWriteContext(int type, JsonWriteContext parent, DupDetector dups) super(); _type = type; _parent = parent; + _nestingDepth = parent == null ? 0 : parent._nestingDepth + 1; _dups = dups; _index = -1; } @@ -79,6 +80,7 @@ protected JsonWriteContext(int type, JsonWriteContext parent, DupDetector dups, super(); _type = type; _parent = parent; + _nestingDepth = parent == null ? 0 : parent._nestingDepth + 1; _dups = dups; _index = -1; _currentValue = currValue; diff --git a/src/main/java/com/fasterxml/jackson/core/json/UTF8JsonGenerator.java b/src/main/java/com/fasterxml/jackson/core/json/UTF8JsonGenerator.java index 3966d52f82..553fb7be93 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/UTF8JsonGenerator.java +++ b/src/main/java/com/fasterxml/jackson/core/json/UTF8JsonGenerator.java @@ -312,6 +312,7 @@ public final void writeStartArray() throws IOException { _verifyValueWrite("start an array"); _writeContext = _writeContext.createChildArrayContext(); + streamWriteConstraints().validateNestingDepth(_writeContext.getNestingDepth()); if (_cfgPrettyPrinter != null) { _cfgPrettyPrinter.writeStartArray(this); } else { @@ -327,6 +328,7 @@ public final void writeStartArray(Object currentValue) throws IOException { _verifyValueWrite("start an array"); _writeContext = _writeContext.createChildArrayContext(currentValue); + streamWriteConstraints().validateNestingDepth(_writeContext.getNestingDepth()); if (_cfgPrettyPrinter != null) { _cfgPrettyPrinter.writeStartArray(this); } else { @@ -342,6 +344,7 @@ public void writeStartArray(Object currentValue, int size) throws IOException { _verifyValueWrite("start an array"); _writeContext = _writeContext.createChildArrayContext(currentValue); + streamWriteConstraints().validateNestingDepth(_writeContext.getNestingDepth()); if (_cfgPrettyPrinter != null) { _cfgPrettyPrinter.writeStartArray(this); } else { @@ -374,6 +377,7 @@ public final void writeStartObject() throws IOException { _verifyValueWrite("start an object"); _writeContext = _writeContext.createChildObjectContext(); + streamWriteConstraints().validateNestingDepth(_writeContext.getNestingDepth()); if (_cfgPrettyPrinter != null) { _cfgPrettyPrinter.writeStartObject(this); } else { @@ -389,6 +393,7 @@ public void writeStartObject(Object forValue) throws IOException { _verifyValueWrite("start an object"); JsonWriteContext ctxt = _writeContext.createChildObjectContext(forValue); + streamWriteConstraints().validateNestingDepth(ctxt.getNestingDepth()); _writeContext = ctxt; if (_cfgPrettyPrinter != null) { _cfgPrettyPrinter.writeStartObject(this); diff --git a/src/main/java/com/fasterxml/jackson/core/json/WriterBasedJsonGenerator.java b/src/main/java/com/fasterxml/jackson/core/json/WriterBasedJsonGenerator.java index 01ccf3bfca..b1628c92b3 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/WriterBasedJsonGenerator.java +++ b/src/main/java/com/fasterxml/jackson/core/json/WriterBasedJsonGenerator.java @@ -251,6 +251,7 @@ public void writeStartArray() throws IOException { _verifyValueWrite("start an array"); _writeContext = _writeContext.createChildArrayContext(); + streamWriteConstraints().validateNestingDepth(_writeContext.getNestingDepth()); if (_cfgPrettyPrinter != null) { _cfgPrettyPrinter.writeStartArray(this); } else { @@ -266,6 +267,7 @@ public void writeStartArray(Object currentValue) throws IOException { _verifyValueWrite("start an array"); _writeContext = _writeContext.createChildArrayContext(currentValue); + streamWriteConstraints().validateNestingDepth(_writeContext.getNestingDepth()); if (_cfgPrettyPrinter != null) { _cfgPrettyPrinter.writeStartArray(this); } else { @@ -281,6 +283,7 @@ public void writeStartArray(Object currentValue, int size) throws IOException { _verifyValueWrite("start an array"); _writeContext = _writeContext.createChildArrayContext(currentValue); + streamWriteConstraints().validateNestingDepth(_writeContext.getNestingDepth()); if (_cfgPrettyPrinter != null) { _cfgPrettyPrinter.writeStartArray(this); } else { @@ -313,6 +316,7 @@ public void writeStartObject() throws IOException { _verifyValueWrite("start an object"); _writeContext = _writeContext.createChildObjectContext(); + streamWriteConstraints().validateNestingDepth(_writeContext.getNestingDepth()); if (_cfgPrettyPrinter != null) { _cfgPrettyPrinter.writeStartObject(this); } else { @@ -328,6 +332,7 @@ public void writeStartObject(Object forValue) throws IOException { _verifyValueWrite("start an object"); JsonWriteContext ctxt = _writeContext.createChildObjectContext(forValue); + streamWriteConstraints().validateNestingDepth(_writeContext.getNestingDepth()); _writeContext = ctxt; if (_cfgPrettyPrinter != null) { _cfgPrettyPrinter.writeStartObject(this); diff --git a/src/main/java/com/fasterxml/jackson/core/util/JsonGeneratorDelegate.java b/src/main/java/com/fasterxml/jackson/core/util/JsonGeneratorDelegate.java index 85c8a0e519..5879063d62 100644 --- a/src/main/java/com/fasterxml/jackson/core/util/JsonGeneratorDelegate.java +++ b/src/main/java/com/fasterxml/jackson/core/util/JsonGeneratorDelegate.java @@ -186,6 +186,17 @@ public JsonGenerator setPrettyPrinter(PrettyPrinter pp) { public JsonGenerator setRootValueSeparator(SerializableString sep) { delegate.setRootValueSeparator(sep); return this; } + /* + /********************************************************************** + /* Constraints violation checking (2.16) + /********************************************************************** + */ + + @Override + public StreamWriteConstraints streamWriteConstraints() { + return delegate.streamWriteConstraints(); + } + /* /********************************************************************** /* Public API, write methods, structural diff --git a/src/test/java/com/fasterxml/jackson/core/write/UTF8GeneratorTest.java b/src/test/java/com/fasterxml/jackson/core/write/UTF8GeneratorTest.java index 469fe58325..1e83778040 100644 --- a/src/test/java/com/fasterxml/jackson/core/write/UTF8GeneratorTest.java +++ b/src/test/java/com/fasterxml/jackson/core/write/UTF8GeneratorTest.java @@ -3,6 +3,7 @@ import java.io.*; import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.core.exc.StreamConstraintsException; import com.fasterxml.jackson.core.filter.FilteringGeneratorDelegate; import com.fasterxml.jackson.core.filter.JsonPointerBasedFilter; import com.fasterxml.jackson.core.filter.TokenFilter.Inclusion; @@ -18,8 +19,7 @@ public class UTF8GeneratorTest extends BaseTest public void testUtf8Issue462() throws Exception { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - IOContext ioc = new IOContext(StreamReadConstraints.defaults(), - new BufferRecycler(), + IOContext ioc = new IOContext(new BufferRecycler(), ContentReference.rawReference(bytes), true); JsonGenerator gen = new UTF8JsonGenerator(ioc, 0, null, bytes, '"'); String str = "Natuurlijk is alles gelukt en weer een tevreden klant\uD83D\uDE04"; @@ -44,6 +44,43 @@ public void testUtf8Issue462() throws Exception p.close(); } + public void testNestingDepthWithSmallLimit() throws Exception + { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + IOContext ioc = new IOContext(null, + StreamWriteConstraints.builder().maxNestingDepth(1).build(), + new BufferRecycler(), + ContentReference.rawReference(bytes), true); + try (JsonGenerator gen = new UTF8JsonGenerator(ioc, 0, null, bytes, '"')) { + gen.writeStartObject(); + gen.writeFieldName("array"); + gen.writeStartArray(); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException sce) { + String expected = "Document nesting depth (2) exceeds the maximum allowed (1, from `StreamWriteConstraints.getMaxNestingDepth()`)"; + assertEquals(expected, sce.getMessage()); + } + } + + public void testNestingDepthWithSmallLimitNestedObject() throws Exception + { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + IOContext ioc = new IOContext(null, + StreamWriteConstraints.builder().maxNestingDepth(1).build(), + new BufferRecycler(), + ContentReference.rawReference(bytes), true); + try (JsonGenerator gen = new UTF8JsonGenerator(ioc, 0, null, bytes, '"')) { + gen.writeStartObject(); + gen.writeFieldName("object"); + gen.writeStartObject(); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException sce) { + String expected = "Document nesting depth (2) exceeds the maximum allowed (1, from `StreamWriteConstraints.getMaxNestingDepth()`)"; + assertEquals(expected, sce.getMessage()); + } + } + + // for [core#115] public void testSurrogatesWithRaw() throws Exception { diff --git a/src/test/java/com/fasterxml/jackson/core/write/WriterBasedJsonGeneratorTest.java b/src/test/java/com/fasterxml/jackson/core/write/WriterBasedJsonGeneratorTest.java new file mode 100644 index 0000000000..3f4eb63ec1 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/write/WriterBasedJsonGeneratorTest.java @@ -0,0 +1,55 @@ +package com.fasterxml.jackson.core.write; + +import com.fasterxml.jackson.core.BaseTest; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.StreamWriteConstraints; +import com.fasterxml.jackson.core.exc.StreamConstraintsException; +import com.fasterxml.jackson.core.io.ContentReference; +import com.fasterxml.jackson.core.io.IOContext; +import com.fasterxml.jackson.core.json.WriterBasedJsonGenerator; +import com.fasterxml.jackson.core.util.BufferRecycler; + +import java.io.StringWriter; + +public class WriterBasedJsonGeneratorTest extends BaseTest +{ + private final JsonFactory JSON_F = new JsonFactory(); + + public void testNestingDepthWithSmallLimit() throws Exception + { + StringWriter sw = new StringWriter(); + IOContext ioc = new IOContext(null, + StreamWriteConstraints.builder().maxNestingDepth(1).build(), + new BufferRecycler(), + ContentReference.rawReference(sw), true); + try (JsonGenerator gen = new WriterBasedJsonGenerator(ioc, 0, null, sw, '"')) { + gen.writeStartObject(); + gen.writeFieldName("array"); + gen.writeStartArray(); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException sce) { + String expected = "Document nesting depth (2) exceeds the maximum allowed (1, from `StreamWriteConstraints.getMaxNestingDepth()`)"; + assertEquals(expected, sce.getMessage()); + } + } + + public void testNestingDepthWithSmallLimitNestedObject() throws Exception + { + StringWriter sw = new StringWriter(); + IOContext ioc = new IOContext(null, + StreamWriteConstraints.builder().maxNestingDepth(1).build(), + new BufferRecycler(), + ContentReference.rawReference(sw), true); + try (JsonGenerator gen = new WriterBasedJsonGenerator(ioc, 0, null, sw, '"')) { + gen.writeStartObject(); + gen.writeFieldName("object"); + gen.writeStartObject(); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException sce) { + String expected = "Document nesting depth (2) exceeds the maximum allowed (1, from `StreamWriteConstraints.getMaxNestingDepth()`)"; + assertEquals(expected, sce.getMessage()); + } + } + +}