From 8151614fc65afbb06a7f065a8b4e2492ca30b3d9 Mon Sep 17 00:00:00 2001 From: Ruben Quesada Lopez Date: Thu, 17 Sep 2026 15:52:35 +0100 Subject: [PATCH] [CALCITE-7790] XmlFunctions: XMLTRANSFORM should parse its XML argument through the shared DocumentBuilder and set explicit JAXP external-access attributes on the TransformerFactory --- .../apache/calcite/runtime/XmlFunctions.java | 13 ++++++- .../calcite/test/SqlXmlFunctionsTest.java | 38 +++++++++++++++++++ site/_docs/security_threat_model.md | 6 +-- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/runtime/XmlFunctions.java b/core/src/main/java/org/apache/calcite/runtime/XmlFunctions.java index 00660a1b4643..f2e199265766 100644 --- a/core/src/main/java/org/apache/calcite/runtime/XmlFunctions.java +++ b/core/src/main/java/org/apache/calcite/runtime/XmlFunctions.java @@ -86,6 +86,13 @@ public class XmlFunctions { } catch (TransformerConfigurationException e) { throw new IllegalStateException("Transformer Factory configuration failed", e); } + try { + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + } catch (IllegalArgumentException e) { + throw new IllegalStateException("Transformer Factory does not support restricting" + + " access to external DTDs and stylesheets", e); + } return transformerFactory; }); @@ -147,14 +154,18 @@ private XmlFunctions() { } try { final Source xsltSource = new StreamSource(new StringReader(xslt)); - final Source xmlSource = new StreamSource(new StringReader(xml)); final Transformer transformer = TRANSFORMER_FACTORY.get().newTransformer(xsltSource); + final Source xmlSource = new DOMSource(getDocumentNode(xml)); final StringWriter writer = new StringWriter(); final StreamResult result = new StreamResult(writer); transformer.setErrorListener(new InternalErrorListener()); transformer.transform(xmlSource, result); return writer.toString(); + } catch (IllegalArgumentException e) { + // getDocumentNode rejected the XML argument (e.g. it contains a + // DOCTYPE declaration, or is not well-formed). + throw RESOURCE.invalidInputForXmlTransform(xml).ex(); } catch (TransformerConfigurationException e) { throw RESOURCE.illegalXslt(xslt).ex(); } catch (TransformerException e) { diff --git a/core/src/test/java/org/apache/calcite/test/SqlXmlFunctionsTest.java b/core/src/test/java/org/apache/calcite/test/SqlXmlFunctionsTest.java index 046d93570591..52cd4a707df7 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlXmlFunctionsTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlXmlFunctionsTest.java @@ -94,6 +94,44 @@ class SqlXmlFunctionsTest { assertXmlTransformFailed(XML, xsltExternalEntity, Matchers.expectThrowable(expected)); } + @Test void testXmlTransformDocumentFunctionDenied() { + String xslt = "" + + "" + + "" + + ""; + String message = "Invalid input for XMLTRANSFORM xml: '" + XML + "'"; + CalciteException expected = new CalciteException(message, null); + assertXmlTransformFailed(XML, xslt, Matchers.expectThrowable(expected)); + } + + @Test void testXmlTransformXslIncludeDenied() { + String xslt = "" + + "" + + ""; + String message = "Illegal xslt specified : '" + xslt + "'"; + CalciteException expected = new CalciteException(message, null); + assertXmlTransformFailed(XML, xslt, Matchers.expectThrowable(expected)); + } + + @Test void testXmlTransformXslImportDenied() { + String xslt = "" + + "" + + ""; + String message = "Illegal xslt specified : '" + xslt + "'"; + CalciteException expected = new CalciteException(message, null); + assertXmlTransformFailed(XML, xslt, Matchers.expectThrowable(expected)); + } + + @Test void testXmlTransformDoctypeDenied() { + String xml = "]>&a;"; + String message = "Invalid input for XMLTRANSFORM xml: '" + xml + "'"; + CalciteException expected = new CalciteException(message, null); + assertXmlTransformFailed(xml, XSLT, Matchers.expectThrowable(expected)); + } + @Test void testXmlTransform() { assertXmlTransform(null, "", nullValue()); assertXmlTransform("", null, nullValue()); diff --git a/site/_docs/security_threat_model.md b/site/_docs/security_threat_model.md index acc22f367bba..1b5e72493b09 100644 --- a/site/_docs/security_threat_model.md +++ b/site/_docs/security_threat_model.md @@ -240,9 +240,9 @@ land. carries a `CancelFlag` in the planner context, so a deadline can build on it; the firing and size caps are new. * **Execution.** Catastrophic regex backtracking in `LIKE`, `SIMILAR TO`, or - `RLIKE`, or an unbounded join, exhausts resources at run time. Planning bounds - do not help here; the mitigation is a match-time limit or a backtracking-free - regex engine. + `RLIKE`, an unbounded XSLT program in `XMLTRANSFORM`, or an unbounded join, + exhausts resources at run time. Planning bounds do not help here; the + mitigation is a match-time limit or a backtracking-free regex engine. * **Parsing.** Deeply nested expressions can overflow the parser stack. The mitigation is a nesting-depth limit.