From 2a9043f0c6f68fe937ba7ab642db1f187ffeaedc Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Wed, 23 Sep 2026 13:19:52 +0530 Subject: [PATCH] enforce maxStringLength/maxNameLength read constraints in XML parser --- .../dataformat/xml/deser/FromXmlParser.java | 2 +- .../dataformat/xml/deser/XmlTokenStream.java | 36 +++++- .../xml/dos/StringAndNameLengthTest.java | 117 ++++++++++++++++++ 3 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 src/test/java/tools/jackson/dataformat/xml/dos/StringAndNameLengthTest.java diff --git a/src/main/java/tools/jackson/dataformat/xml/deser/FromXmlParser.java b/src/main/java/tools/jackson/dataformat/xml/deser/FromXmlParser.java index 2d8fb94b..722eb644 100644 --- a/src/main/java/tools/jackson/dataformat/xml/deser/FromXmlParser.java +++ b/src/main/java/tools/jackson/dataformat/xml/deser/FromXmlParser.java @@ -209,7 +209,7 @@ public FromXmlParser(ObjectReadContext readCtxt, IOContext ioCtxt, parserFeatures, xmlFeatures, xmlReader, new XmlTokenStream(xmlReader, ioCtxt.contentReference(), - xmlFeatures, nameProcessor), + xmlFeatures, nameProcessor, ioCtxt.streamReadConstraints()), nameProcessor, nameForTextElement); } diff --git a/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java b/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java index 705ca8a6..baa40923 100644 --- a/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java +++ b/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java @@ -10,6 +10,7 @@ import org.codehaus.stax2.XMLStreamLocation2; import org.codehaus.stax2.XMLStreamReader2; +import tools.jackson.core.StreamReadConstraints; import tools.jackson.core.TokenStreamLocation; import tools.jackson.core.exc.StreamReadException; import tools.jackson.core.io.ContentReference; @@ -86,6 +87,15 @@ public class XmlTokenStream protected XmlNameProcessor _nameProcessor; + /** + * Read constraints used to enforce {@link StreamReadConstraints#validateStringLength} + * on element text / attribute values and {@link StreamReadConstraints#validateNameLength} + * on element / attribute names. + * + * @since 3.3 + */ + protected final StreamReadConstraints _streamReadConstraints; + /* /********************************************************************** /* Parsing state @@ -193,6 +203,17 @@ public class XmlTokenStream public XmlTokenStream(XMLStreamReader xmlReader, ContentReference sourceRef, int formatFeatures, XmlNameProcessor nameProcessor) + { + this(xmlReader, sourceRef, formatFeatures, nameProcessor, + StreamReadConstraints.defaults()); + } + + /** + * @since 3.3 + */ + public XmlTokenStream(XMLStreamReader xmlReader, ContentReference sourceRef, + int formatFeatures, XmlNameProcessor nameProcessor, + StreamReadConstraints streamReadConstraints) { _sourceReference = sourceRef; _formatFeatures = formatFeatures; @@ -202,6 +223,7 @@ public XmlTokenStream(XMLStreamReader xmlReader, ContentReference sourceRef, // 04-Dec-2023, tatu: [dataformat-xml#618] Need further customized adapter: _xmlReader = Stax2JacksonReaderAdapter.wrapIfNecessary(xmlReader); _nameProcessor = nameProcessor; + _streamReadConstraints = streamReadConstraints; } /** @@ -343,7 +365,15 @@ public void skipEndElement() throws IOException, XMLStreamException public int getCurrentToken() { return _currentState; } - public String getText() { return _textValue; } + public String getText() { + // Honor `StreamReadConstraints.maxStringLength` for element text and + // attribute values (idempotent: cheap length check even when this accessor + // is called more than once for the same value) + if (_textValue != null) { + _streamReadConstraints.validateStringLength(_textValue.length()); + } + return _textValue; + } /** * Accessor for local name of current named event (that is, @@ -847,6 +877,8 @@ private static boolean _isXsiNilTrue(String value) { * @since 2.14 */ protected void _decodeElementName(String namespaceURI, String localName) { + // Honor `StreamReadConstraints.maxNameLength` for element names + _streamReadConstraints.validateNameLength(localName.length()); // 31-Jan-2024, tatu: [dataformat-xml#634] Need to convert 'xsi:type'? // (not 100% sure if needed for elements but let's do for now) if (_cfgProcessXsiType) { @@ -867,6 +899,8 @@ protected void _decodeElementName(String namespaceURI, String localName) { * @since 2.14 */ protected void _decodeAttributeName(String namespaceURI, String localName) { + // Honor `StreamReadConstraints.maxNameLength` for attribute names + _streamReadConstraints.validateNameLength(localName.length()); // 31-Jan-2024, tatu: [dataformat-xml#634] Need to convert 'xsi:type'? if (_cfgProcessXsiType) { if (localName.equals("type") && XSI_NAMESPACE.equals(namespaceURI)) { diff --git a/src/test/java/tools/jackson/dataformat/xml/dos/StringAndNameLengthTest.java b/src/test/java/tools/jackson/dataformat/xml/dos/StringAndNameLengthTest.java new file mode 100644 index 00000000..f1beda8b --- /dev/null +++ b/src/test/java/tools/jackson/dataformat/xml/dos/StringAndNameLengthTest.java @@ -0,0 +1,117 @@ +package tools.jackson.dataformat.xml.dos; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.JsonParser; +import tools.jackson.core.StreamReadConstraints; +import tools.jackson.core.exc.StreamConstraintsException; + +import tools.jackson.dataformat.xml.*; + +import static org.junit.jupiter.api.Assertions.*; + +// Verifies that `StreamReadConstraints.maxStringLength` / `maxNameLength` are +// honored when reading XML (element text, attribute values, and element / +// attribute / root names), matching the JSON backend. +public class StringAndNameLengthTest extends XmlTestUtil +{ + private XmlMapper mapperWithStringLimit(int max) { + return mapperBuilder(XmlFactory.builder() + .streamReadConstraints(StreamReadConstraints.builder() + .maxStringLength(max).build()) + .build()).build(); + } + + private XmlMapper mapperWithNameLimit(int max) { + return mapperBuilder(XmlFactory.builder() + .streamReadConstraints(StreamReadConstraints.builder() + .maxNameLength(max).build()) + .build()).build(); + } + + private void drain(XmlMapper mapper, String xml) throws Exception { + try (JsonParser p = mapper.createParser(xml)) { + while (p.nextToken() != null) { + p.getString(); + } + } + } + + @Test + public void testElementTextLength() throws Exception + { + final XmlMapper mapper = mapperWithStringLimit(100); + final String value = "x".repeat(5000); + try { + drain(mapper, "" + value + ""); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException e) { + assertTrue(e.getMessage().contains("String value length (5000) exceeds the maximum allowed"), + "Unexpected message: " + e.getMessage()); + } + } + + @Test + public void testAttributeValueLength() throws Exception + { + final XmlMapper mapper = mapperWithStringLimit(100); + final String value = "x".repeat(5000); + try { + drain(mapper, ""); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException e) { + assertTrue(e.getMessage().contains("String value length (5000) exceeds the maximum allowed"), + "Unexpected message: " + e.getMessage()); + } + } + + @Test + public void testElementNameLength() throws Exception + { + final XmlMapper mapper = mapperWithNameLimit(50); + final String name = "n".repeat(5000); + try { + drain(mapper, "<" + name + ">v"); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException e) { + assertTrue(e.getMessage().contains("Name length (5000) exceeds the maximum allowed"), + "Unexpected message: " + e.getMessage()); + } + } + + @Test + public void testAttributeNameLength() throws Exception + { + final XmlMapper mapper = mapperWithNameLimit(50); + final String name = "n".repeat(5000); + try { + drain(mapper, ""); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException e) { + assertTrue(e.getMessage().contains("Name length (5000) exceeds the maximum allowed"), + "Unexpected message: " + e.getMessage()); + } + } + + @Test + public void testRootElementNameLength() throws Exception + { + final XmlMapper mapper = mapperWithNameLimit(50); + final String name = "n".repeat(5000); + try { + drain(mapper, "<" + name + ">v"); + fail("expected StreamConstraintsException"); + } catch (StreamConstraintsException e) { + assertTrue(e.getMessage().contains("Name length (5000) exceeds the maximum allowed"), + "Unexpected message: " + e.getMessage()); + } + } + + // Values within the configured limits must parse unchanged. + @Test + public void testWithinLimits() throws Exception + { + drain(mapperWithStringLimit(100), "hello world"); + drain(mapperWithNameLimit(50), "text"); + } +}