This is an automated email from the ASF dual-hosted git repository. Croway pushed a commit to branch camel-4.18.x in repository https://gitbox.apache.org/repos/asf/camel.git
commit 1b17b24b0424e70971b5e9b48c430f66a70efdaf Author: Andrea Cosentino <[email protected]> AuthorDate: Mon Aug 31 10:24:07 2026 +0200 CAMEL-24475: camel-xpath - parse an InputSource document type with the hardened XML parser (#25683) XPathBuilder handed an InputSource straight to XPathExpression, which builds a DocumentBuilder of its own with the JDK defaults - so documentType=InputSource (and SAXSource) accepted a DOCTYPE declaration and resolved external entities, while the default documentType of Document did not. All four evaluation sites now convert through the type converter, reusing the same hardened DocumentBuilderFactory the default document type already goes through. This adds no document parse: evaluate(InputSource) already built a full DOM internally. (cherry picked from commit 1ead256f1bfcef36c2572a88809bcd7547bdc111) Co-authored-by: Claude Opus 5 (1M context) <[email protected]> --- .../apache/camel/language/xpath/XPathBuilder.java | 33 ++++++++-- .../apache/camel/builder/xml/XPathFeatureTest.java | 72 ++++++++++++++++++++++ 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/components/camel-xpath/src/main/java/org/apache/camel/language/xpath/XPathBuilder.java b/components/camel-xpath/src/main/java/org/apache/camel/language/xpath/XPathBuilder.java index 80f9ebfe30d3..10d5cf59a358 100644 --- a/components/camel-xpath/src/main/java/org/apache/camel/language/xpath/XPathBuilder.java +++ b/components/camel-xpath/src/main/java/org/apache/camel/language/xpath/XPathBuilder.java @@ -941,7 +941,8 @@ public class XPathBuilder extends ServiceSupport // fetch all namespaces if (document instanceof InputSource) { InputSource inputSource = (InputSource) document; - answer = (NodeList) xpathExpression.evaluate(inputSource, XPathConstants.NODESET); + answer = (NodeList) xpathExpression.evaluate(toHardenedDocument(exchange, inputSource), + XPathConstants.NODESET); } else if (document instanceof DOMSource) { DOMSource source = (DOMSource) document; answer = (NodeList) xpathExpression.evaluate(source.getNode(), XPathConstants.NODESET); @@ -949,7 +950,8 @@ public class XPathBuilder extends ServiceSupport SAXSource source = (SAXSource) document; // since its a SAXSource it may not return an NodeList (for // example if using Saxon) - Object result = xpathExpression.evaluate(source.getInputSource(), XPathConstants.NODESET); + Object result = xpathExpression.evaluate(toHardenedDocument(exchange, source.getInputSource()), + XPathConstants.NODESET); if (result instanceof NodeList) { answer = (NodeList) result; } else { @@ -1017,7 +1019,7 @@ public class XPathBuilder extends ServiceSupport } if (document instanceof InputSource) { InputSource inputSource = (InputSource) document; - answer = xpathExpression.evaluate(inputSource, resultQName); + answer = xpathExpression.evaluate(toHardenedDocument(exchange, inputSource), resultQName); } else if (document instanceof DOMSource) { DOMSource source = (DOMSource) document; answer = xpathExpression.evaluate(source.getNode(), resultQName); @@ -1027,7 +1029,7 @@ public class XPathBuilder extends ServiceSupport } else { if (document instanceof InputSource) { InputSource inputSource = (InputSource) document; - answer = xpathExpression.evaluate(inputSource); + answer = xpathExpression.evaluate(toHardenedDocument(exchange, inputSource)); } else if (document instanceof DOMSource) { DOMSource source = (DOMSource) document; answer = xpathExpression.evaluate(source.getNode()); @@ -1228,6 +1230,29 @@ public class XPathBuilder extends ServiceSupport return false; } + /** + * Parses an {@link InputSource} into a DOM document before it is evaluated. + * <p> + * {@link XPathExpression#evaluate(InputSource)} and its overloads build a {@link javax.xml.parsers.DocumentBuilder} + * of their own using the JDK defaults, which accept a {@code DOCTYPE} declaration and resolve external entities. + * Routing the source through the type converter instead reuses the hardened {@code DocumentBuilderFactory} that the + * default {@code documentType} of {@link Document} already goes through, so both document types are parsed with the + * same configuration. The XPath engine builds a full DOM from the source either way, so this does not add a parse + * that was not already happening. + */ + protected Document toHardenedDocument(Exchange exchange, InputSource inputSource) { + Document document = null; + if (inputSource != null) { + document = exchange.getContext().getTypeConverter().convertTo(Document.class, exchange, inputSource); + } + if (document == null) { + throw new RuntimeCamelException( + "Cannot convert the InputSource to a org.w3c.dom.Document for evaluating the XPath expression: " + + getText()); + } + return document; + } + /** * Strategy method to extract the document from the exchange. */ diff --git a/core/camel-core/src/test/java/org/apache/camel/builder/xml/XPathFeatureTest.java b/core/camel-core/src/test/java/org/apache/camel/builder/xml/XPathFeatureTest.java index d40fdc35177d..2422701d920e 100644 --- a/core/camel-core/src/test/java/org/apache/camel/builder/xml/XPathFeatureTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/builder/xml/XPathFeatureTest.java @@ -16,8 +16,13 @@ */ package org.apache.camel.builder.xml; +import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.xml.sax.InputSource; import org.xml.sax.SAXParseException; import org.apache.camel.ContextTestSupport; @@ -26,17 +31,23 @@ import org.apache.camel.NoTypeConversionAvailableException; import org.apache.camel.RuntimeCamelException; import org.apache.camel.TypeConversionException; import org.apache.camel.converter.jaxp.XmlConverter; +import org.apache.camel.language.xpath.XPathBuilder; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.ResourceLock; import org.junit.jupiter.api.parallel.Resources; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.camel.language.xpath.XPathBuilder.xpath; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.*; @ResourceLock(Resources.SYSTEM_PROPERTIES) public class XPathFeatureTest extends ContextTestSupport { public static final String DOM_BUILDER_FACTORY_FEATURE = XmlConverter.DOCUMENT_BUILDER_FACTORY_FEATURE; + private static final String CANARY = "CANARY-SHOULD-NOT-BE-READ"; + public static final String XML_DATA = " <!DOCTYPE foo [ " + " <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM \"file:///bin/test.sh\" >]> <test> &xxe; </test>"; public static final String XML_DATA_INVALID @@ -76,6 +87,67 @@ public class XPathFeatureTest extends ContextTestSupport { } } + /** + * {@code documentType=InputSource} used to hand the payload straight to {@link javax.xml.xpath.XPathExpression}, + * which builds a DocumentBuilder of its own with the JDK defaults - so the DOCTYPE that + * {@link #testXPathDocTypeDisallowed()} pins as refused on the default document type was accepted here, and the + * external entity was resolved and expanded into the evaluated document. The two document types must agree on the + * parser configuration. + */ + @Test + void docTypeIsAlsoDisallowedForAnInputSourceDocumentType() throws Exception { + Path secret = Files.createTempFile("camel-xpath-entity", ".txt"); + try { + Files.writeString(secret, CANARY); + // an InputStream body, since that is what converts to an InputSource - and what a streaming + // documentType=InputSource deployment actually receives + String xml = "<!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe SYSTEM \"" + + secret.toUri() + "\" >]> <test> &xxe; </test>"; + + // both branches of doInEvaluateAs: with a result QName and without one + for (XPathBuilder builder : List.of(xpath("/").documentType(InputSource.class).stringResult(), + xpath("/test").documentType(InputSource.class))) { + assertThatThrownBy(() -> builder.evaluate(createExchange(new ByteArrayInputStream(xml.getBytes(UTF_8))))) + .as("a DOCTYPE must be refused for documentType=InputSource, as it is for the default type") + .hasRootCauseInstanceOf(SAXParseException.class) + .rootCause().hasMessageContaining("DOCTYPE"); + } + } finally { + Files.deleteIfExists(secret); + } + } + + /** + * The {@code InputSource} document type now shares the default type's parser, so it also shares its escape hatch: + * the same system properties that {@link #testXPath()} uses relax it. Points at a file that does not exist, so a + * {@code FileNotFoundException} is what proves the DOCTYPE was accepted and resolution attempted. + */ + @Test + void theDocumentBuilderFactoryFeaturesAlsoRelaxTheInputSourceDocumentType() { + System.setProperty(DOM_BUILDER_FACTORY_FEATURE + ":" + "http://xml.org/sax/features/external-general-entities", "true"); + System.setProperty(DOM_BUILDER_FACTORY_FEATURE + ":" + "http://apache.org/xml/features/disallow-doctype-decl", "false"); + try { + assertThatThrownBy(() -> xpath("/").documentType(InputSource.class).stringResult() + .evaluate(createExchange(new ByteArrayInputStream(XML_DATA.getBytes(UTF_8))))) + .hasRootCauseInstanceOf(FileNotFoundException.class); + } finally { + System.clearProperty(DOM_BUILDER_FACTORY_FEATURE + ":" + "http://xml.org/sax/features/external-general-entities"); + System.clearProperty(DOM_BUILDER_FACTORY_FEATURE + ":" + "http://apache.org/xml/features/disallow-doctype-decl"); + } + } + + /** + * Guards the assumption the test above rests on: an {@code InputStream} body really does reach the + * {@code InputSource} branch, rather than failing earlier for want of a type converter. + */ + @Test + void anInputStreamBodyConvertsToAnInputSourceDocumentType() { + Object result = xpath("/test/text()").documentType(InputSource.class).stringResult() + .evaluate(createExchange(new ByteArrayInputStream("<test>ok</test>".getBytes(UTF_8)))); + + assertThat(result).isEqualTo("ok"); + } + @Test public void testXPathNoTypeConverter() { // define a class without type converter as document type
