diff --git a/core/camel-xml-jaxp/src/main/java/org/apache/camel/converter/jaxp/XmlConverter.java b/core/camel-xml-jaxp/src/main/java/org/apache/camel/converter/jaxp/XmlConverter.java index fdb98e05e0022..86402fec674b5 100644 --- a/core/camel-xml-jaxp/src/main/java/org/apache/camel/converter/jaxp/XmlConverter.java +++ b/core/camel-xml-jaxp/src/main/java/org/apache/camel/converter/jaxp/XmlConverter.java @@ -75,6 +75,7 @@ import org.apache.camel.Converter; import org.apache.camel.Exchange; import org.apache.camel.StreamCache; +import org.apache.camel.TypeConversionException; import org.apache.camel.support.CamelContextHelper; import org.apache.camel.util.IOHelper; import org.apache.camel.util.ObjectHelper; @@ -696,6 +697,14 @@ public Element toDOMElement(Node node) throws TransformerException { @Converter(order = 54) public Document toDOMDocument(byte[] data, Exchange exchange) throws IOException, SAXException, ParserConfigurationException { + if (!looksLikeXml(data)) { + throw new TypeConversionException( + data, Document.class, + new IllegalArgumentException( + "Payload does not start with a valid XML prolog" + + " (first bytes do not look like XML —" + + " possible causes: empty body, JSON/HTML error response, wrong encoding)")); + } DocumentBuilder documentBuilder = createDocumentBuilder(getDocumentBuilderFactory(exchange)); return documentBuilder.parse(new ByteArrayInputStream(data)); } @@ -703,8 +712,16 @@ public Document toDOMDocument(byte[] data, Exchange exchange) @Converter(order = 55) public Document toDOMDocument(StreamCache cache, Exchange exchange) throws IOException, SAXException, ParserConfigurationException { - InputStream is = exchange.getContext().getTypeConverter().convertTo(InputStream.class, exchange, cache); - return toDOMDocument(is, exchange); + byte[] data = exchange.getContext().getTypeConverter().convertTo(byte[].class, exchange, cache); + if (!looksLikeXml(data)) { + throw new TypeConversionException( + data, Document.class, + new IllegalArgumentException( + "Payload does not start with a valid XML prolog" + + " (first bytes do not look like XML —" + + " possible causes: empty body, JSON/HTML error response, wrong encoding)")); + } + return toDOMDocument(data, exchange); } /** @@ -1226,4 +1243,56 @@ public void fatalError(SAXParseException exception) throws SAXException { LOG.error(exception.getMessage(), exception); } } + + /** + * Returns {@code true} if the given byte array looks like it could be well-formed XML, by inspecting only the first + * few bytes. + *
+ * The check handles: + *
+ * This is intentionally a fast, conservative check: it returns {@code true} for anything that could be XML + * (starts with {@code <}), which means it will not reject e.g. HTML. Its only purpose is to avoid handing obviously + * non-XML content (empty, JSON, plain-text error bodies) to {@code DocumentBuilder.parse()}, which would throw a + * {@code SAXParseException: Content is not allowed in prolog}. + * + * @param data the bytes to inspect (may be null or empty) + * @return {@code true} if the content may be XML; {@code false} if it is definitely not XML + */ + static boolean looksLikeXml(byte[] data) { + if (data == null || data.length == 0) { + return false; + } + int offset = 0; + // skip UTF-8 BOM (EF BB BF) + if (data.length >= 3 + && (data[0] & 0xFF) == 0xEF + && (data[1] & 0xFF) == 0xBB + && (data[2] & 0xFF) == 0xBF) { + offset = 3; + } else if (data.length >= 2) { + // UTF-16 BE (FE FF) or LE (FF FE) BOM — XML parsers handle these natively, accept as-is + int b0 = data[0] & 0xFF; + int b1 = data[1] & 0xFF; + if ((b0 == 0xFE && b1 == 0xFF) || (b0 == 0xFF && b1 == 0xFE)) { + return true; + } + } + // skip leading ASCII whitespace + while (offset < data.length) { + byte b = data[offset]; + if (b == ' ' || b == '\t' || b == '\r' || b == '\n') { + offset++; + } else { + break; + } + } + return offset < data.length && data[offset] == '<'; + } + } diff --git a/core/camel-xml-jaxp/src/test/java/org/apache/camel/converter/jaxp/XmlConverterPrologTest.java b/core/camel-xml-jaxp/src/test/java/org/apache/camel/converter/jaxp/XmlConverterPrologTest.java new file mode 100644 index 0000000000000..7470bfe3be7e2 --- /dev/null +++ b/core/camel-xml-jaxp/src/test/java/org/apache/camel/converter/jaxp/XmlConverterPrologTest.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.converter.jaxp; + +import java.nio.charset.StandardCharsets; + +import org.apache.camel.TypeConversionException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies the prolog-guard in {@link XmlConverter}. + * + *
+ * {@link XmlConverter#looksLikeXml(byte[])} unit tests confirm the helper correctly identifies XML vs non-XML content. + * Integration tests via {@link XmlConverter#toDOMDocument(byte[], Exchange)} confirm that non-XML payloads throw + * {@link TypeConversionException} (explicit, diagnosable failure) rather than propagating a + * {@code SAXParseException: Content is not allowed in prolog} from deep inside the JDK parser. + * + *
+ * Real-world trigger: {@code ByteArrayInputStreamCache} carrying a non-XML HTTP response body (JSON error page, empty
+ * body, BOM-only) routed through {@code CxfPayloadConverter} ->
+ * {@code XmlConverter.toDOMDocument(StreamCache, Exchange)}.
+ */
+class XmlConverterPrologTest {
+
+ // ---- looksLikeXml unit tests ----
+
+ @Test
+ void looksLikeXml_nullReturnsFalse() {
+ assertFalse(XmlConverter.looksLikeXml(null));
+ }
+
+ @Test
+ void looksLikeXml_emptyReturnsFalse() {
+ assertFalse(XmlConverter.looksLikeXml(new byte[0]));
+ }
+
+ @Test
+ void looksLikeXml_jsonBodyReturnsFalse() {
+ assertFalse(XmlConverter.looksLikeXml("{\"error\":\"bad request\"}".getBytes(StandardCharsets.UTF_8)));
+ }
+
+ @Test
+ void looksLikeXml_plainTextReturnsFalse() {
+ assertFalse(XmlConverter.looksLikeXml("some plain text".getBytes(StandardCharsets.UTF_8)));
+ }
+
+ @Test
+ void looksLikeXml_httpStatusLineReturnsFalse() {
+ // typical non-XML upstream response: HTTP status line or JSON error body
+ assertFalse(XmlConverter.looksLikeXml(
+ "HTTP/1.1 500 Internal Server Error".getBytes(StandardCharsets.UTF_8)));
+ }
+
+ @Test
+ void looksLikeXml_utf8BomOnlyReturnsFalse() {
+ byte[] bomOnly = { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF };
+ assertFalse(XmlConverter.looksLikeXml(bomOnly));
+ }
+
+ @Test
+ void looksLikeXml_utf8BomFollowedByJsonReturnsFalse() {
+ byte[] bom = { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF };
+ byte[] body = "{\"k\":\"v\"}".getBytes(StandardCharsets.UTF_8);
+ byte[] data = new byte[bom.length + body.length];
+ System.arraycopy(bom, 0, data, 0, bom.length);
+ System.arraycopy(body, 0, data, bom.length, body.length);
+ assertFalse(XmlConverter.looksLikeXml(data));
+ }
+
+ @Test
+ void looksLikeXml_validXmlDeclarationReturnsTrue() {
+ assertTrue(XmlConverter.looksLikeXml(
+ "