This is an automated email from the ASF dual-hosted git repository.

jamesnetherton pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git


The following commit(s) were added to refs/heads/main by this push:
     new 9a570b6497 Apply Camel's JAXP external access restrictions in the xslt 
extension
9a570b6497 is described below

commit 9a570b64977b0e24f85d67c2b2220aeac9fa5274
Author: James Netherton <[email protected]>
AuthorDate: Tue Sep 8 07:06:04 2026 +0100

    Apply Camel's JAXP external access restrictions in the xslt extension
    
    * Fixes #9115. Apply Camel's JAXP external access restrictions in the xslt 
extension
    
    Camel restricts access to external DTDs and stylesheets on the 
TransformerFactory
    it creates. The xslt extension supplies its own factory, so those 
restrictions
    were not applied to the transformations it performs. The factory is also
    registered as the JAXP default, so this covers any code obtaining one 
through
    TransformerFactory.newInstance(), including applications using
    camel-quarkus-tika and camel-quarkus-xmlsecurity, which depend on the same
    support extension without using XSLT themselves.
    
    XalanTransformerFactory now applies the restrictions itself:
    
    * Documents being transformed are parsed with an XMLReader that does not 
resolve
      external general entities, matching what Camel's XmlConverter does for the
      bodies camel-xslt converts to a SAXSource itself. A SAXSource carrying a 
caller
      configured XMLReader is used as it is, and DOMSource and StAXSource are 
already
      parsed.
    * Resources fetched at transform time by document() are denied unless the
      application's own URIResolver resolves them. Xalan does not propagate the
      factory resolver onto everything it hands out, so it is installed on each 
entry
      point; an application setting its own resolver, which camel-xslt does on 
every
      exchange, keeps overriding it as before.
    
    xsl:import and xsl:include are unchanged. Xalan dereferences those hrefs 
itself
    while compiling, so they cannot be restricted here, which matches what plain
    Camel does on the component path. A test pins that behaviour so a Xalan 
upgrade
    changing it does not go unnoticed.
    
    CamelXsltRecorder applies quarkus.camel.xslt.features to every template 
instead
    of only to those compiled to a translet at build time, where it previously 
had
    no effect on templates loaded at runtime, and warns once when secure 
processing
    is disabled.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    
    * Keep the external access restriction on transformers reached via SAX
    
    TransformerHandler and TrAXFilter hand out the transformer they go on to use
    themselves, so resetting it, or clearing its URIResolver, took the 
restriction
    off the handler from underneath it. The JDK is unaffected by either, 
because it
    does not depend on a resolver to enforce external access, so this was the 
last
    place the extension was not on a par with it.
    
    Both are now wrapped so that getTransformer() returns the same secured
    transformer the Transformer and Templates entry points hand out, which 
reinstates
    the restriction after reset() and treats a null resolver as a fallback to it
    rather than as a removal. Xalan reads its own transformer from a field and 
never
    calls getTransformer(), so overriding it does not disturb the 
transformation.
    
    The handler wrapper implements DeclHandler as well, since Xalan's own 
handler
    does and a caller may install it as one.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../modules/ROOT/pages/migration-guide/3.40.0.adoc |  57 +++
 .../ROOT/pages/reference/extensions/xslt.adoc      |  30 ++
 extensions-support/xalan/deployment/pom.xml        |   6 +
 .../XalanTransformerFactoryExternalAccessTest.java | 448 +++++++++++++++++++
 .../support/xalan/XalanTransformerFactory.java     | 490 ++++++++++++++++++++-
 .../xslt/runtime/src/main/doc/configuration.adoc   |  29 ++
 .../quarkus/component/xslt/CamelXsltRecorder.java  |  35 +-
 7 files changed, 1076 insertions(+), 19 deletions(-)

diff --git a/docs/modules/ROOT/pages/migration-guide/3.40.0.adoc 
b/docs/modules/ROOT/pages/migration-guide/3.40.0.adoc
index e5cd536d82..099532dae0 100644
--- a/docs/modules/ROOT/pages/migration-guide/3.40.0.adoc
+++ b/docs/modules/ROOT/pages/migration-guide/3.40.0.adoc
@@ -96,3 +96,60 @@ Note also that a configured list takes precedence over 
Camel's own default exclu
 A warning is now logged at startup for the two that weaken verification if the 
operator assumes otherwise, instead of them being dropped silently. In 
particular, a Camel component using a bridged bean does not check certificate 
revocation, and so accepts a revoked certificate that the rest of the 
application rejects.
 
 Refer to the 
xref:reference/extensions/tls-registry.adoc#extensions-tls-registry-usage-protocols-and-cipher-suites[TLS
 Registry extension documentation] for details.
+
+== XSLT extension changes
+
+The `camel-quarkus-xslt` extension transforms with Xalan-J rather than the 
XSLT implementation built into the JDK, because translets have to be compiled 
ahead of time to work in native mode. Xalan-J 2.7.x predates JAXP 1.5 and 
cannot honour `javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD` or 
`ACCESS_EXTERNAL_STYLESHEET`, and its secure processing feature restricts 
extension functions only, without implying the external access restrictions the 
JDK applies under the same feature. Those restric [...]
+
+This affects more than the `xslt:` endpoints. The extension registers its 
factory as the JAXP default, so any code in the application that obtains a 
`TransformerFactory` through `TransformerFactory.newInstance()` is transforming 
with it, including code reached through `camel-quarkus-tika` and 
`camel-quarkus-xmlsecurity`, which depend on the same support extension without 
using XSLT themselves. Refer to the 
xref:reference/extensions/xslt.adoc#extensions-xslt-configuration-external-access[
 [...]
+
+=== External entities in input documents are no longer resolved
+
+Documents being transformed are now parsed with an `XMLReader` that does not 
resolve external general entities, so a `SYSTEM` entity in a `DOCTYPE` 
declaration no longer expands into the transformation result.
+
+Most routes see no change, because camel-xslt already converts `String`, 
`byte[]` and `InputStream` bodies through Camel's own hardened parser before 
the transformer sees them. The change matters where the body reaches the 
transformer already shaped as a `Source`, for example after 
`convertBodyTo(Source.class)` or from a component that produces one, and for 
code calling the factory directly.
+
+A `SAXSource` carrying an `XMLReader` the caller configured is passed through 
untouched, so an application that needs different parsing rules can supply its 
own reader.
+
+[source,java]
+----
+SAXParserFactory parserFactory = SAXParserFactory.newInstance();
+parserFactory.setNamespaceAware(true);
+// Configure the parser as required, then hand the transformer a reader of 
your own
+Source source = new SAXSource(parserFactory.newSAXParser().getXMLReader(), 
inputSource);
+----
+
+`DOMSource` and `StAXSource` bodies are parsed before they reach the 
transformer and are unaffected.
+
+=== document() is denied unless a URIResolver resolves it
+
+Resources fetched at transform time by the XSLT `document()` function are now 
denied unless the application's own `javax.xml.transform.URIResolver` resolves 
them, which is what the JDK does when secure processing is enabled.
+
+Routes are unaffected. camel-xslt installs a `URIResolver` on every 
transformer it uses, so `document()` continues to resolve exactly as it did, 
and as it does on plain Camel. The change affects code that uses the JAXP 
default factory directly and relies on `document()` without setting a resolver.
+
+The restriction applies to every entry point that hands out something to 
transform with, so code driving the SAX push API through 
`newTransformerHandler()` or `newXMLFilter()` is restricted in the same way, 
including the `Transformer` reached through 
`TransformerHandler.getTransformer()`.
+
+Because the restriction is carried by a `URIResolver` rather than by a factory 
attribute Xalan cannot honour, it survives the two things that would otherwise 
remove it: `Transformer.reset()` keeps it, and `setURIResolver(null)` falls 
back to it rather than lifting it. An application that needs `document()` to 
resolve sets a resolver of its own, as before.
+
+[source,java]
+----
+Transformer transformer = 
TransformerFactory.newInstance().newTransformer(stylesheet);
+transformer.setURIResolver(myResolver);
+----
+
+`xsl:import` and `xsl:include` are unchanged. Xalan dereferences those hrefs 
itself while compiling a stylesheet, ignoring the resolver it consulted first, 
so they cannot be restricted here. Stylesheets are deployment owned rather than 
attacker controlled, and camel-xslt resolves includes through its own 
unrestricted resolver in any case.
+
+=== quarkus.camel.xslt.features applies to every template
+
+`quarkus.camel.xslt.features` was only applied to templates that were compiled 
to a translet at build time, which is `classpath:` templates. It was silently 
ignored for templates loaded at runtime, which is the remaining schemes in JVM 
mode. It is now applied to every template the component transforms with.
+
+A feature that previously had no effect on those endpoints now takes effect, 
and one the `TransformerFactory` does not support now fails endpoint creation 
rather than passing unnoticed. Review the property if it is set for an 
application that transforms with templates loaded at runtime.
+
+Disabling secure processing also now logs a warning on startup, since it 
permits templates to call Xalan extension functions.
+
+[source,properties]
+----
+quarkus.camel.xslt.features."http\://javax.xml.XMLConstants/feature/secure-processing"=false
+----
+
+IMPORTANT: Only disable it where every template the application transforms 
with is trusted.
diff --git a/docs/modules/ROOT/pages/reference/extensions/xslt.adoc 
b/docs/modules/ROOT/pages/reference/extensions/xslt.adoc
index a49659fe0e..d3482d8a00 100644
--- a/docs/modules/ROOT/pages/reference/extensions/xslt.adoc
+++ b/docs/modules/ROOT/pages/reference/extensions/xslt.adoc
@@ -85,6 +85,36 @@ TransformerFactory features can be configured using 
following property:
 ----
 
quarkus.camel.xslt.features."http\://javax.xml.XMLConstants/feature/secure-processing"=false
 ----
+
+Features are applied to every template the component transforms with, whether 
it was compiled to a translet at
+build time or loaded at runtime. A feature the `TransformerFactory` does not 
support fails endpoint creation.
+
+WARNING: Disabling secure-processing permits templates to call Xalan extension 
functions, and is logged as a
+warning on startup. Only do this where every template the application 
transforms with is trusted.
+
+[id="extensions-xslt-configuration-external-access"]
+=== External access
+The extension transforms with Xalan-J rather than the XSLT implementation 
built into the JDK, because translets
+have to be compiled ahead of time to work in native mode. Xalan-J 2.7.x 
predates JAXP 1.5 and cannot honour
+`javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD` or `ACCESS_EXTERNAL_STYLESHEET`, 
so `setAttribute` throws for both,
+and its secure-processing feature restricts extension functions without 
implying the external access
+restrictions the JDK applies under the same feature. The extension applies 
those restrictions itself:
+
+* Documents being transformed are parsed with an `XMLReader` that does not 
resolve external general entities, so
+a `SYSTEM` entity in a `DOCTYPE` declaration does not expand into the result. 
A `SAXSource` carrying an
+`XMLReader` the caller configured is used as it is.
+* Resources fetched at transform time by the `document()` function are denied 
unless a
+`javax.xml.transform.URIResolver` resolves them. This applies to every entry 
point that hands out something to
+transform with, so a `TransformerHandler` or an `XMLFilter` is restricted just 
as a `Transformer` is. The
+component installs a resolver on every transformer it uses, so routes resolve 
`document()` as they do on plain
+Camel.
+
+`<xsl:import>` and `<xsl:include>` are not restricted. Xalan dereferences 
those hrefs itself while compiling a
+stylesheet, ignoring the resolver it consulted first. Stylesheets are 
deployment owned rather than attacker
+controlled, and the component resolves includes through its own unrestricted 
resolver in any case.
+
+NOTE: This factory is registered as the JAXP default, so the restrictions 
above also apply to code in the
+application that obtains a `TransformerFactory` through 
`TransformerFactory.newInstance()`.
 [id="extensions-xslt-configuration-extension-functions-support"]
 === Extension functions support
 https://xml.apache.org/xalan-j/extensions.html[Xalan's extension functions]
diff --git a/extensions-support/xalan/deployment/pom.xml 
b/extensions-support/xalan/deployment/pom.xml
index f5e9c7ec9a..5691aff73e 100644
--- a/extensions-support/xalan/deployment/pom.xml
+++ b/extensions-support/xalan/deployment/pom.xml
@@ -41,6 +41,12 @@
             <groupId>org.apache.camel.quarkus</groupId>
             <artifactId>camel-quarkus-support-xalan</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>io.quarkus</groupId>
+            <artifactId>quarkus-junit-internal</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git 
a/extensions-support/xalan/deployment/src/test/java/org/apache/camel/quarkus/support/xalan/deployment/XalanTransformerFactoryExternalAccessTest.java
 
b/extensions-support/xalan/deployment/src/test/java/org/apache/camel/quarkus/support/xalan/deployment/XalanTransformerFactoryExternalAccessTest.java
new file mode 100644
index 0000000000..29d6f88a9b
--- /dev/null
+++ 
b/extensions-support/xalan/deployment/src/test/java/org/apache/camel/quarkus/support/xalan/deployment/XalanTransformerFactoryExternalAccessTest.java
@@ -0,0 +1,448 @@
+/*
+ * 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.quarkus.support.xalan.deployment;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.URIResolver;
+import javax.xml.transform.sax.SAXSource;
+import javax.xml.transform.sax.SAXTransformerFactory;
+import javax.xml.transform.sax.TransformerHandler;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+
+import org.xml.sax.InputSource;
+import org.xml.sax.XMLFilter;
+import org.xml.sax.XMLReader;
+
+import org.apache.camel.quarkus.support.xalan.XalanTransformerFactory;
+import org.apache.xalan.xsltc.trax.TrAXFilter;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Xalan-J 2.7.x cannot honour {@link XMLConstants#ACCESS_EXTERNAL_DTD} and
+ * {@link XMLConstants#ACCESS_EXTERNAL_STYLESHEET}, and its
+ * {@link XMLConstants#FEATURE_SECURE_PROCESSING} does not imply them either. 
These tests pin down the
+ * external access restrictions {@link XalanTransformerFactory} applies in 
their place, so that the
+ * behaviour stays in line with what upstream Camel gets from the JDK factory.
+ */
+class XalanTransformerFactoryExternalAccessTest {
+
+    private static final String SECRET = "TOP-SECRET-CONTENT";
+
+    private static final String INCLUDED_XSL = "<xsl:stylesheet version='1.0'"
+            + " xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>"
+            + "<xsl:template 
name='included'><xsl:text>INCLUDED</xsl:text></xsl:template></xsl:stylesheet>";
+
+    @TempDir
+    static Path tempDir;
+
+    private static String secretUri;
+    private static String secretXmlUri;
+    private static String includedUri;
+
+    @BeforeAll
+    static void writeExternalResources() throws IOException {
+        final Path secret = tempDir.resolve("secret.txt");
+        Files.writeString(secret, SECRET);
+        secretUri = secret.toUri().toString();
+
+        // document() parses what it fetches, so its target has to be well 
formed XML for the test to fail
+        // when the restriction is absent rather than when the content cannot 
be parsed
+        final Path secretXml = tempDir.resolve("secret.xml");
+        Files.writeString(secretXml, "<s>" + SECRET + "</s>");
+        secretXmlUri = secretXml.toUri().toString();
+
+        final Path included = tempDir.resolve("included.xsl");
+        Files.writeString(included, INCLUDED_XSL);
+        includedUri = included.toUri().toString();
+    }
+
+    /** Copies the value of {@code //data} into the output, so an expanded 
entity would show up there */
+    private static final String COPY_DATA_XSL = "<xsl:stylesheet version='1.0'"
+            + " xmlns:xsl='http://www.w3.org/1999/XSL/Transform'><xsl:output 
method='text'/>"
+            + "<xsl:template match='/'><xsl:value-of 
select='//data'/></xsl:template></xsl:stylesheet>";
+
+    private static String externalEntityDocument() {
+        return "<?xml version='1.0'?><!DOCTYPE r [<!ENTITY xxe SYSTEM '" + 
secretUri + "'>]>"
+                + "<r><data>&xxe;</data></r>";
+    }
+
+    private static String documentFunctionXsl() {
+        return "<xsl:stylesheet version='1.0' 
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>"
+                + "<xsl:output method='text'/><xsl:template match='/'>"
+                + "<xsl:value-of select=\"document('" + secretXmlUri + 
"')\"/></xsl:template></xsl:stylesheet>";
+    }
+
+    private static String includingXsl() {
+        return "<xsl:stylesheet version='1.0' 
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>"
+                + "<xsl:include href='" + includedUri + "'/><xsl:output 
method='text'/>"
+                + "<xsl:template match='/'><xsl:call-template 
name='included'/></xsl:template></xsl:stylesheet>";
+    }
+
+    private static String transform(TransformerFactory factory, String xsl, 
Source input) throws Exception {
+        final Transformer transformer = factory.newTemplates(new 
StreamSource(new StringReader(xsl))).newTransformer();
+        final StringWriter result = new StringWriter();
+        transformer.transform(input, new StreamResult(result));
+        return result.toString();
+    }
+
+    /** Carries a value so that a push through the SAX wrappers can be 
asserted on, not only denied */
+    private static final String PUSHED_DOCUMENT = "<r><data>HELLO</data></r>";
+
+    private static Source stylesheetSource() {
+        return new StreamSource(new StringReader(documentFunctionXsl()));
+    }
+
+    /** Either the transform fails or it yields nothing; what must not happen 
is the secret coming back */
+    private static void assertDenied(ThrowingSupplier<String> transformation) {
+        String result;
+        try {
+            result = transformation.get();
+        } catch (Exception e) {
+            // A refusal Xalan reports as a failure rather than as an empty 
result
+            return;
+        }
+        assertFalse(result.contains(SECRET), "An external resource was 
resolved into the transformation result");
+    }
+
+    private static String 
pushThroughHandler(SaxFactoryFunction<TransformerHandler> handlerFactory) 
throws Exception {
+        return pushThroughHandler(new XalanTransformerFactory(), 
handlerFactory);
+    }
+
+    /**
+     * Drives a {@link TransformerHandler} the way the SAX push API is meant 
to be used: the caller parses the
+     * input document with a reader of its own and feeds the events in.
+     */
+    private static String pushThroughHandler(XalanTransformerFactory factory,
+            SaxFactoryFunction<TransformerHandler> handlerFactory) throws 
Exception {
+        final TransformerHandler handler = handlerFactory.apply(factory);
+        final StringWriter result = new StringWriter();
+        handler.setResult(new StreamResult(result));
+
+        final XMLReader reader = namespaceAwareReader();
+        reader.setContentHandler(handler);
+        reader.parse(new InputSource(new StringReader(PUSHED_DOCUMENT)));
+        return result.toString();
+    }
+
+    private static String pushThroughFilter(SaxFactoryFunction<XMLFilter> 
filterFactory) throws Exception {
+        return pushThroughFilter(new XalanTransformerFactory(), filterFactory);
+    }
+
+    private static String pushThroughFilter(XalanTransformerFactory factory,
+            SaxFactoryFunction<XMLFilter> filterFactory) throws Exception {
+        final XMLFilter filter = filterFactory.apply(factory);
+        final StringWriter result = new StringWriter();
+
+        // The filter transforms and passes the events on; an identity handler 
serialises what comes out
+        final TransformerHandler output = (TransformerHandler) 
((SAXTransformerFactory) TransformerFactory
+                
.newInstance("org.apache.xalan.xsltc.trax.TransformerFactoryImpl", 
null)).newTransformerHandler();
+        output.setResult(new StreamResult(result));
+
+        filter.setParent(namespaceAwareReader());
+        filter.setContentHandler(output);
+        filter.parse(new InputSource(new StringReader(PUSHED_DOCUMENT)));
+        return result.toString();
+    }
+
+    private static XMLReader namespaceAwareReader() throws Exception {
+        final SAXParserFactory parserFactory = SAXParserFactory.newInstance();
+        parserFactory.setNamespaceAware(true);
+        return parserFactory.newSAXParser().getXMLReader();
+    }
+
+    @FunctionalInterface
+    private interface SaxFactoryFunction<T> {
+        T apply(SAXTransformerFactory factory) throws Exception;
+    }
+
+    @FunctionalInterface
+    private interface ThrowingSupplier<T> {
+        T get() throws Exception;
+    }
+
+    /**
+     * A body that is already {@link Source} shaped bypasses the hardened SAX 
conversion camel-xslt applies to
+     * String, byte[] and InputStream bodies, so the factory has to parse it 
safely itself.
+     */
+    @Test
+    void externalEntityInSourceShapedInputIsNotResolved() throws Exception {
+        final String result = transform(new XalanTransformerFactory(), 
COPY_DATA_XSL,
+                new StreamSource(new StringReader(externalEntityDocument())));
+
+        assertFalse(result.contains(SECRET), "The external entity was resolved 
into the transformation result");
+    }
+
+    /** The same document reaching the transformer the way camel-xslt hands it 
over must stay safe too */
+    @Test
+    void externalEntityInSaxSourceInputIsNotResolved() throws Exception {
+        final SAXParserFactory saxParserFactory = 
SAXParserFactory.newInstance();
+        saxParserFactory.setNamespaceAware(true);
+        saxParserFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, 
true);
+        
saxParserFactory.setFeature("http://xml.org/sax/features/external-general-entities";,
 false);
+        final SAXSource saxSource = new 
SAXSource(saxParserFactory.newSAXParser().getXMLReader(),
+                new InputSource(new StringReader(externalEntityDocument())));
+
+        final String result = transform(new XalanTransformerFactory(), 
COPY_DATA_XSL, saxSource);
+
+        assertFalse(result.contains(SECRET), "The external entity was resolved 
into the transformation result");
+    }
+
+    /**
+     * A {@link StreamSource} carrying nothing but a system id has no stream 
for the factory to wrap, so it
+     * would otherwise be handed to Xalan to open and parse with a parser of 
its own.
+     */
+    @Test
+    void externalEntityInSystemIdOnlyStreamSourceIsNotResolved() throws 
Exception {
+        final Path document = tempDir.resolve("external-entity.xml");
+        Files.writeString(document, externalEntityDocument());
+
+        final String result = transform(new XalanTransformerFactory(), 
COPY_DATA_XSL,
+                new StreamSource(document.toUri().toString()));
+
+        assertFalse(result.contains(SECRET), "The external entity was resolved 
into the transformation result");
+    }
+
+    /** Loading a document by system id has to keep working, entities aside */
+    @Test
+    void systemIdOnlyStreamSourceIsStillTransformed() throws Exception {
+        final Path document = tempDir.resolve("plain.xml");
+        Files.writeString(document, "<r><data>HELLO</data></r>");
+
+        final String result = transform(new XalanTransformerFactory(), 
COPY_DATA_XSL,
+                new StreamSource(document.toUri().toString()));
+
+        assertEquals("HELLO", result.trim());
+    }
+
+    /**
+     * {@link Transformer#reset()} restores the configuration the transformer 
was created with, which drops
+     * the {@link URIResolver} carrying the restriction. Resetting must not 
hand back an unrestricted
+     * transformer.
+     */
+    @Test
+    void documentFunctionStaysDeniedAfterTransformerReset() throws Exception {
+        final Transformer transformer = new XalanTransformerFactory()
+                .newTemplates(new StreamSource(new 
StringReader(documentFunctionXsl()))).newTransformer();
+        transformer.reset();
+
+        assertDenied(() -> {
+            final StringWriter result = new StringWriter();
+            transformer.transform(new StreamSource(new StringReader("<r/>")), 
new StreamResult(result));
+            return result.toString();
+        });
+    }
+
+    /**
+     * Clearing the resolver leaves the JDK restricted, since it does not 
depend on one to enforce external
+     * access. Here the resolver is the only means of enforcement, so a null 
must not lift the restriction.
+     */
+    @Test
+    void documentFunctionStaysDeniedWhenUriResolverIsCleared() throws 
Exception {
+        final Transformer transformer = new XalanTransformerFactory()
+                .newTemplates(new StreamSource(new 
StringReader(documentFunctionXsl()))).newTransformer();
+        transformer.setURIResolver(null);
+
+        assertDenied(() -> {
+            final StringWriter result = new StringWriter();
+            transformer.transform(new StreamSource(new StringReader("<r/>")), 
new StreamResult(result));
+            return result.toString();
+        });
+    }
+
+    /** Hardening the input document must not stop ordinary transformations 
from working */
+    @Test
+    void ordinaryTransformationIsUnaffected() throws Exception {
+        final String result = transform(new XalanTransformerFactory(), 
COPY_DATA_XSL,
+                new StreamSource(new 
StringReader("<r><data>HELLO</data></r>")));
+
+        assertEquals("HELLO", result.trim());
+    }
+
+    /**
+     * {@code document()} is resolved at transform time, where Xalan does not 
consult the factory's
+     * {@link URIResolver} on its own. Whether the refusal surfaces as an 
exception or as an empty result is
+     * Xalan's business; what matters is that the resource is not fetched.
+     */
+    @Test
+    void documentFunctionIsDeniedWithoutApplicationUriResolver() {
+        assertDenied(() -> transform(new XalanTransformerFactory(), 
documentFunctionXsl(),
+                new StreamSource(new StringReader("<r/>"))));
+    }
+
+    /**
+     * {@code document()} has to be denied on every entry point handing out 
something to transform with, not
+     * only on {@link Transformer} and {@link javax.xml.transform.Templates}. 
Xalan copies the factory's
+     * {@link URIResolver} onto some of these and not others, so each one is 
pinned separately.
+     */
+    @Test
+    void documentFunctionIsDeniedInTransformerHandlerFromSource() {
+        assertDenied(() -> pushThroughHandler(factory -> 
factory.newTransformerHandler(stylesheetSource())));
+    }
+
+    @Test
+    void documentFunctionIsDeniedInTransformerHandlerFromTemplates() {
+        assertDenied(() -> pushThroughHandler(
+                factory -> 
factory.newTransformerHandler(factory.newTemplates(stylesheetSource()))));
+    }
+
+    @Test
+    void documentFunctionIsDeniedInXmlFilterFromSource() {
+        assertDenied(() -> pushThroughFilter(factory -> 
factory.newXMLFilter(stylesheetSource())));
+    }
+
+    @Test
+    void documentFunctionIsDeniedInXmlFilterFromTemplates() {
+        assertDenied(() -> pushThroughFilter(
+                factory -> 
factory.newXMLFilter(factory.newTemplates(stylesheetSource()))));
+    }
+
+    /**
+     * Xalan hands out the very transformer the handler goes on to use, so a 
caller that resets it, or clears
+     * its resolver, would otherwise take the restriction off the handler from 
underneath it. The JDK is
+     * immune to both because it does not need a {@link URIResolver} to 
enforce external access.
+     */
+    @Test
+    void documentFunctionStaysDeniedAfterResettingTheHandlersTransformer() {
+        assertDenied(() -> pushThroughHandler(factory -> {
+            final TransformerHandler handler = 
factory.newTransformerHandler(stylesheetSource());
+            handler.getTransformer().reset();
+            return handler;
+        }));
+    }
+
+    @Test
+    void documentFunctionStaysDeniedWhenTheHandlersResolverIsCleared() {
+        assertDenied(() -> pushThroughHandler(factory -> {
+            final TransformerHandler handler = 
factory.newTransformerHandler(stylesheetSource());
+            handler.getTransformer().setURIResolver(null);
+            return handler;
+        }));
+    }
+
+    @Test
+    void documentFunctionStaysDeniedAfterResettingTheFiltersTransformer() {
+        assertDenied(() -> pushThroughFilter(factory -> {
+            final XMLFilter filter = factory.newXMLFilter(stylesheetSource());
+            ((TrAXFilter) filter).getTransformer().reset();
+            return filter;
+        }));
+    }
+
+    /**
+     * The wrappers that make the above possible sit on the SAX event path, so 
an ordinary push through each
+     * of them has to keep producing the same output it did before.
+     */
+    @Test
+    void transformerHandlerStillTransforms() throws Exception {
+        final String result = pushThroughHandler(
+                factory -> factory.newTransformerHandler(new StreamSource(new 
StringReader(COPY_DATA_XSL))));
+
+        assertEquals("HELLO", result.trim());
+    }
+
+    @Test
+    void xmlFilterStillTransforms() throws Exception {
+        final String result = pushThroughFilter(
+                factory -> factory.newXMLFilter(new StreamSource(new 
StringReader(COPY_DATA_XSL))));
+
+        assertTrue(result.contains("HELLO"), "The XMLFilter did not transform 
the document: " + result);
+    }
+
+    /**
+     * The counterpart to the four tests above: an application that resolves 
the reference itself still gets
+     * it, which is also what proves those tests deny a fetch rather than 
merely exercising a broken path.
+     */
+    @Test
+    void applicationUriResolverResolvesDocumentInXmlFilter() throws Exception {
+        final XalanTransformerFactory factory = new XalanTransformerFactory();
+        factory.setURIResolver((href, base) -> new StreamSource(new 
StringReader("<s>" + SECRET + "</s>"), href));
+
+        assertTrue(pushThroughFilter(factory, f -> 
f.newXMLFilter(stylesheetSource())).contains(SECRET),
+                "The application URIResolver did not resolve document()");
+    }
+
+    /**
+     * Xalan dereferences an {@code xsl:include} href itself whenever it can, 
ignoring both a refusal and a
+     * {@code null} from the {@link URIResolver} it consulted first, so 
compile time includes cannot be
+     * restricted here. Pinned so that a Xalan upgrade changing this does not 
go unnoticed. Stylesheets are
+     * deployment owned, and camel-xslt resolves includes through its own 
unrestricted resolver anyway.
+     */
+    @Test
+    void externalIncludeCannotBeRestricted() throws Exception {
+        final String result = transform(new XalanTransformerFactory(), 
includingXsl(),
+                new StreamSource(new StringReader("<r/>")));
+
+        assertEquals("INCLUDED", result.trim());
+    }
+
+    /**
+     * camel-xslt resolves includes through its own {@link URIResolver} (and 
camel-quarkus resolves them
+     * through {@code BuildTimeUriResolver} at build time). Those must keep 
working, otherwise the extension
+     * would be stricter than plain Camel rather than on a par with it. The 
href deliberately uses a scheme
+     * Xalan cannot dereference on its own, so only the application resolver 
can satisfy it.
+     */
+    @Test
+    void applicationUriResolverResolvesIncludesXalanCannot() throws Exception {
+        final TransformerFactory factory = new XalanTransformerFactory();
+        factory.setURIResolver((href, base) -> 
"buildtime:included.xsl".equals(href)
+                ? new StreamSource(new StringReader(INCLUDED_XSL), href)
+                : null);
+
+        final String xsl = "<xsl:stylesheet version='1.0' 
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>"
+                + "<xsl:include href='buildtime:included.xsl'/><xsl:output 
method='text'/>"
+                + "<xsl:template match='/'><xsl:call-template 
name='included'/></xsl:template></xsl:stylesheet>";
+
+        assertEquals("INCLUDED", transform(factory, xsl, new StreamSource(new 
StringReader("<r/>"))).trim());
+    }
+
+    /** The resolver an application sets must be the one that is handed back 
to it */
+    @Test
+    void applicationUriResolverIsVisibleToTheApplication() {
+        final TransformerFactory factory = new XalanTransformerFactory();
+        final URIResolver resolver = (href, base) -> null;
+        factory.setURIResolver(resolver);
+
+        assertEquals(resolver, factory.getURIResolver());
+    }
+
+    /**
+     * Secure processing is what the external access restrictions above stand 
in for, so it must stay on by
+     * default.
+     */
+    @Test
+    void secureProcessingIsEnabledByDefault() {
+        assertDoesNotThrow(() -> new 
XalanTransformerFactory().getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+    }
+}
diff --git 
a/extensions-support/xalan/runtime/src/main/java/org/apache/camel/quarkus/support/xalan/XalanTransformerFactory.java
 
b/extensions-support/xalan/runtime/src/main/java/org/apache/camel/quarkus/support/xalan/XalanTransformerFactory.java
index 13f1ceff65..1635a33ae9 100644
--- 
a/extensions-support/xalan/runtime/src/main/java/org/apache/camel/quarkus/support/xalan/XalanTransformerFactory.java
+++ 
b/extensions-support/xalan/runtime/src/main/java/org/apache/camel/quarkus/support/xalan/XalanTransformerFactory.java
@@ -16,7 +16,15 @@
  */
 package org.apache.camel.quarkus.support.xalan;
 
+import java.io.InputStream;
+import java.io.Reader;
+import java.util.Properties;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParserFactory;
 import javax.xml.transform.ErrorListener;
+import javax.xml.transform.Result;
 import javax.xml.transform.Source;
 import javax.xml.transform.Templates;
 import javax.xml.transform.Transformer;
@@ -24,24 +32,67 @@ import 
javax.xml.transform.TransformerConfigurationException;
 import javax.xml.transform.TransformerException;
 import javax.xml.transform.TransformerFactory;
 import javax.xml.transform.URIResolver;
+import javax.xml.transform.sax.SAXSource;
 import javax.xml.transform.sax.SAXTransformerFactory;
 import javax.xml.transform.sax.TemplatesHandler;
 import javax.xml.transform.sax.TransformerHandler;
+import javax.xml.transform.stream.StreamSource;
 
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
 import org.xml.sax.XMLFilter;
+import org.xml.sax.XMLReader;
+import org.xml.sax.ext.DeclHandler;
 
+import org.apache.xalan.xsltc.trax.TrAXFilter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
  * A {@link TransformerFactory} delegating to a {@link TransformerFactory} 
created via
  * {@code 
TransformerFactory.newInstance("org.apache.xalan.xsltc.trax.TransformerFactoryImpl",
 Thread.currentThread().getContextClassLoader())}
+ * <p>
+ * Xalan-J 2.7.x predates JAXP 1.5, so it cannot honour {@link 
XMLConstants#ACCESS_EXTERNAL_DTD} and
+ * {@link XMLConstants#ACCESS_EXTERNAL_STYLESHEET} - {@code setAttribute()} 
throws
+ * {@link IllegalArgumentException} for both. Its {@link 
XMLConstants#FEATURE_SECURE_PROCESSING} limits
+ * extension functions only, and does not imply the external access 
restrictions the JDK applies under the
+ * same feature. Because callers commonly harden a factory with
+ * {@code try { setAttribute(ACCESS_EXTERNAL_DTD, "") } catch (Exception 
ignored) {}}, that hardening would
+ * be lost silently. This factory therefore applies the deny-by-default part 
itself:
+ * <ul>
+ * <li>input documents passed to {@link Transformer#transform(Source, 
javax.xml.transform.Result)} are
+ * parsed with an {@link XMLReader} that does not resolve external general 
entities, which is what upstream
+ * Camel's {@code XmlConverter} does for the bodies it converts to a {@link 
SAXSource} itself,</li>
+ * <li>resources fetched at transform time by the {@code document()} function 
are denied unless the
+ * application's own {@link URIResolver} resolves them, on every entry point 
that hands out something to
+ * transform with - {@link Transformer}, {@link Templates}, {@link 
TransformerHandler} and
+ * {@link XMLFilter}.</li>
+ * </ul>
+ * <p>
+ * One restriction cannot be reinstated here: Xalan resolves {@code 
xsl:import}/{@code xsl:include} directly
+ * whenever the href is one it can dereference, ignoring both a refusal and a 
{@code null} from the
+ * {@link URIResolver} it consulted first. Stylesheets are deployment owned 
rather than attacker controlled,
+ * and camel-xslt resolves includes through its own unrestricted {@code 
XsltUriResolver} anyway, so this
+ * matches what plain Camel does on the component path.
  */
 public final class XalanTransformerFactory extends SAXTransformerFactory {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(XalanTransformerFactory.class);
 
+    private static final String EXTERNAL_GENERAL_ENTITIES = 
"http://xml.org/sax/features/external-general-entities";;
+
     private final SAXTransformerFactory delegate;
 
+    /**
+     * The {@link URIResolver} set by the application, if any. The delegate 
factory keeps
+     * {@link RestrictingUriResolver} installed at all times so that the 
restriction cannot be dropped by
+     * an application calling {@link #setURIResolver(URIResolver)}.
+     */
+    private volatile URIResolver applicationUriResolver;
+
+    private final RestrictingUriResolver restrictingUriResolver = new 
RestrictingUriResolver();
+
     public XalanTransformerFactory() {
         final SAXTransformerFactory factory = (SAXTransformerFactory) 
TransformerFactory.newInstance(
                 "org.apache.xalan.xsltc.trax.TransformerFactoryImpl",
@@ -53,21 +104,33 @@ public final class XalanTransformerFactory extends 
SAXTransformerFactory {
         }
 
         this.delegate = factory;
+        this.delegate.setURIResolver(restrictingUriResolver);
     }
 
     @Override
     public Transformer newTransformer(Source source) throws 
TransformerConfigurationException {
-        return delegate.newTransformer(source);
+        return secure(delegate.newTransformer(source));
     }
 
     @Override
     public Transformer newTransformer() throws 
TransformerConfigurationException {
-        return delegate.newTransformer();
+        return secure(delegate.newTransformer());
     }
 
     @Override
     public Templates newTemplates(Source source) throws 
TransformerConfigurationException {
-        return delegate.newTemplates(source);
+        return new SecuredTemplates(delegate.newTemplates(source), this);
+    }
+
+    /**
+     * Xalan does not propagate the factory's {@link URIResolver} onto the 
transformers it produces, so
+     * {@code document()} would be resolved unrestricted at transform time. 
Installing the resolver here is
+     * what makes {@link XMLConstants#ACCESS_EXTERNAL_STYLESHEET} effective. 
Applications that set their own
+     * resolver on the transformer - camel-xslt does so on every exchange - 
keep overriding it as before.
+     */
+    private Transformer secure(Transformer transformer) {
+        transformer.setURIResolver(restrictingUriResolver);
+        return new SecuredTransformer(transformer, restrictingUriResolver);
     }
 
     @Override
@@ -78,12 +141,13 @@ public final class XalanTransformerFactory extends 
SAXTransformerFactory {
 
     @Override
     public void setURIResolver(URIResolver resolver) {
-        delegate.setURIResolver(resolver);
+        // Keep RestrictingUriResolver on the delegate; it consults this 
resolver first.
+        this.applicationUriResolver = resolver;
     }
 
     @Override
     public URIResolver getURIResolver() {
-        return delegate.getURIResolver();
+        return applicationUriResolver;
     }
 
     @Override
@@ -118,17 +182,17 @@ public final class XalanTransformerFactory extends 
SAXTransformerFactory {
 
     @Override
     public TransformerHandler newTransformerHandler(Source source) throws 
TransformerConfigurationException {
-        return delegate.newTransformerHandler(source);
+        return secure(delegate.newTransformerHandler(source));
     }
 
     @Override
     public TransformerHandler newTransformerHandler(Templates templates) 
throws TransformerConfigurationException {
-        return delegate.newTransformerHandler(templates);
+        return secure(delegate.newTransformerHandler(unwrap(templates)));
     }
 
     @Override
     public TransformerHandler newTransformerHandler() throws 
TransformerConfigurationException {
-        return delegate.newTransformerHandler();
+        return secure(delegate.newTransformerHandler());
     }
 
     @Override
@@ -138,12 +202,418 @@ public final class XalanTransformerFactory extends 
SAXTransformerFactory {
 
     @Override
     public XMLFilter newXMLFilter(Source source) throws 
TransformerConfigurationException {
-        return delegate.newXMLFilter(source);
+        final Templates templates = delegate.newTemplates(source);
+        return templates == null ? null : newXMLFilter(templates);
     }
 
     @Override
     public XMLFilter newXMLFilter(Templates templates) throws 
TransformerConfigurationException {
-        return delegate.newXMLFilter(templates);
+        return new SecuredTrAXFilter(unwrap(templates), 
restrictingUriResolver);
+    }
+
+    /**
+     * The SAX push entry points hand the caller a {@link Transformer} to 
configure rather than one to call,
+     * and Xalan only copies the factory's {@link URIResolver} onto some of 
them, so {@code document()} is
+     * restricted here for the same reason it is in {@link 
#secure(Transformer)}. The document being
+     * transformed is parsed by the {@link XMLReader} the caller drives the 
handler with, which is the
+     * caller's own choice just as a {@link SAXSource} carrying a reader is.
+     * <p>
+     * The handler is wrapped so that {@link 
TransformerHandler#getTransformer()} hands out a
+     * {@link SecuredTransformer}. Xalan hands out the transformer it goes on 
to use itself, so an
+     * unwrapped one would let {@code reset()} or a cleared resolver drop the 
restriction from underneath
+     * the handler. The JDK is unaffected by either because it does not depend 
on a {@link URIResolver} to
+     * enforce {@link XMLConstants#ACCESS_EXTERNAL_STYLESHEET}.
+     */
+    private TransformerHandler secure(TransformerHandler handler) {
+        return new SecuredTransformerHandler(handler, 
secure(handler.getTransformer()));
+    }
+
+    /**
+     * Xalan casts {@link Templates} to its own {@code TemplatesImpl} 
internally, so the wrapper has to be
+     * peeled off before handing one back to the delegate.
+     */
+    private static Templates unwrap(Templates templates) {
+        return templates instanceof SecuredTemplates ? ((SecuredTemplates) 
templates).delegate : templates;
+    }
+
+    /**
+     * Parses {@code source} with a hardened {@link XMLReader} unless it has 
already been parsed, or the
+     * caller supplied its own reader. Mirrors what {@code 
XmlConverter.createSAXParserFactory()} does for
+     * the bodies camel-xslt converts itself, so that {@link Source}-shaped 
bodies get the same treatment.
+     */
+    private static Source secureInputSource(Source source) throws 
TransformerException {
+        if (source instanceof StreamSource) {
+            final StreamSource streamSource = (StreamSource) source;
+            final InputStream inputStream = streamSource.getInputStream();
+            final Reader reader = streamSource.getReader();
+            final String systemId = streamSource.getSystemId();
+            if (inputStream == null && reader == null && systemId == null) {
+                // Nothing to parse; let the delegate report it as it did 
before
+                return source;
+            }
+            final InputSource inputSource = new InputSource();
+            // Carried in every case so that relative references keep 
resolving against the document
+            inputSource.setSystemId(systemId);
+            if (inputStream != null) {
+                inputSource.setByteStream(inputStream);
+            } else if (reader != null) {
+                inputSource.setCharacterStream(reader);
+            }
+            // A source with nothing but a systemId is parsed by the secured 
reader rather than opened by
+            // the delegate, which would otherwise parse the document it 
fetches with its own parser
+            return new SAXSource(createSecureXmlReader(), inputSource);
+        }
+        if (source instanceof SAXSource) {
+            final SAXSource saxSource = (SAXSource) source;
+            if (saxSource.getXMLReader() == null) {
+                return new SAXSource(createSecureXmlReader(), 
saxSource.getInputSource());
+            }
+        }
+        // DOMSource and StAXSource are already parsed; a caller supplied 
XMLReader is the caller's own choice
+        return source;
+    }
+
+    private static XMLReader createSecureXmlReader() throws 
TransformerException {
+        final SAXParserFactory factory = SAXParserFactory.newInstance();
+        factory.setNamespaceAware(true);
+        setFeature(factory, javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, 
true);
+        setFeature(factory, EXTERNAL_GENERAL_ENTITIES, false);
+        try {
+            return factory.newSAXParser().getXMLReader();
+        } catch (ParserConfigurationException | SAXException e) {
+            throw new TransformerException("Could not create a secure 
XMLReader for the input document", e);
+        }
+    }
+
+    private static void setFeature(SAXParserFactory factory, String name, 
boolean value) {
+        try {
+            factory.setFeature(name, value);
+        } catch (ParserConfigurationException | SAXException e) {
+            LOGGER.warn("SAXParserFactory does not support the feature {} with 
value {}, due to {}."
+                    + " External entities in XSLT input documents may be 
resolved.", name, value, e.getMessage());
+        }
+    }
+
+    /**
+     * Denies external references that the application's own {@link 
URIResolver} did not resolve. This is the
+     * {@link XMLConstants#ACCESS_EXTERNAL_STYLESHEET} behaviour the JDK 
applies when
+     * {@link XMLConstants#FEATURE_SECURE_PROCESSING} is enabled, which Xalan 
does not implement. It takes
+     * effect for {@code document()} at transform time; see the class javadoc 
for why compile time
+     * {@code xsl:import}/{@code xsl:include} cannot be covered.
+     */
+    private final class RestrictingUriResolver implements URIResolver {
+        @Override
+        public Source resolve(String href, String base) throws 
TransformerException {
+            final URIResolver resolver = applicationUriResolver;
+            if (resolver != null) {
+                final Source source = resolver.resolve(href, base);
+                if (source != null) {
+                    return source;
+                }
+            }
+            throw new TransformerException(
+                    "Access to the external resource '" + href + "' (base '" + 
base + "') is not allowed."
+                            + " Resolve it through a 
javax.xml.transform.URIResolver if it is required.");
+        }
+    }
+
+    /**
+     * A {@link TrAXFilter} handing out a {@link SecuredTransformer} rather 
than the transformer it filters
+     * with, so that the restriction cannot be taken off the one it uses. 
Xalan reads its own transformer
+     * from a field and never calls {@link #getTransformer()} itself, so 
overriding it is safe.
+     */
+    private static final class SecuredTrAXFilter extends TrAXFilter {
+        private final Transformer securedTransformer;
+
+        SecuredTrAXFilter(Templates templates, URIResolver 
restrictingUriResolver)
+                throws TransformerConfigurationException {
+            super(templates);
+            final Transformer transformer = super.getTransformer();
+            transformer.setURIResolver(restrictingUriResolver);
+            this.securedTransformer = new SecuredTransformer(transformer, 
restrictingUriResolver);
+        }
+
+        @Override
+        public Transformer getTransformer() {
+            return securedTransformer;
+        }
+    }
+
+    /**
+     * Delegates the SAX events straight through, and exists only so that
+     * {@link TransformerHandler#getTransformer()} hands out a {@link 
SecuredTransformer}. Implements
+     * {@link DeclHandler} because Xalan's own handler does, and a caller may 
install it as one.
+     */
+    private static final class SecuredTransformerHandler implements 
TransformerHandler, DeclHandler {
+        private final TransformerHandler delegate;
+        private final Transformer securedTransformer;
+
+        SecuredTransformerHandler(TransformerHandler delegate, Transformer 
securedTransformer) {
+            this.delegate = delegate;
+            this.securedTransformer = securedTransformer;
+        }
+
+        @Override
+        public Transformer getTransformer() {
+            return securedTransformer;
+        }
+
+        @Override
+        public void setResult(Result result) {
+            delegate.setResult(result);
+        }
+
+        @Override
+        public void setSystemId(String systemId) {
+            delegate.setSystemId(systemId);
+        }
+
+        @Override
+        public String getSystemId() {
+            return delegate.getSystemId();
+        }
+
+        @Override
+        public void setDocumentLocator(Locator locator) {
+            delegate.setDocumentLocator(locator);
+        }
+
+        @Override
+        public void startDocument() throws SAXException {
+            delegate.startDocument();
+        }
+
+        @Override
+        public void endDocument() throws SAXException {
+            delegate.endDocument();
+        }
+
+        @Override
+        public void startPrefixMapping(String prefix, String uri) throws 
SAXException {
+            delegate.startPrefixMapping(prefix, uri);
+        }
+
+        @Override
+        public void endPrefixMapping(String prefix) throws SAXException {
+            delegate.endPrefixMapping(prefix);
+        }
+
+        @Override
+        public void startElement(String uri, String localName, String qName, 
Attributes atts) throws SAXException {
+            delegate.startElement(uri, localName, qName, atts);
+        }
+
+        @Override
+        public void endElement(String uri, String localName, String qName) 
throws SAXException {
+            delegate.endElement(uri, localName, qName);
+        }
+
+        @Override
+        public void characters(char[] ch, int start, int length) throws 
SAXException {
+            delegate.characters(ch, start, length);
+        }
+
+        @Override
+        public void ignorableWhitespace(char[] ch, int start, int length) 
throws SAXException {
+            delegate.ignorableWhitespace(ch, start, length);
+        }
+
+        @Override
+        public void processingInstruction(String target, String data) throws 
SAXException {
+            delegate.processingInstruction(target, data);
+        }
+
+        @Override
+        public void skippedEntity(String name) throws SAXException {
+            delegate.skippedEntity(name);
+        }
+
+        @Override
+        public void startDTD(String name, String publicId, String systemId) 
throws SAXException {
+            delegate.startDTD(name, publicId, systemId);
+        }
+
+        @Override
+        public void endDTD() throws SAXException {
+            delegate.endDTD();
+        }
+
+        @Override
+        public void startEntity(String name) throws SAXException {
+            delegate.startEntity(name);
+        }
+
+        @Override
+        public void endEntity(String name) throws SAXException {
+            delegate.endEntity(name);
+        }
+
+        @Override
+        public void startCDATA() throws SAXException {
+            delegate.startCDATA();
+        }
+
+        @Override
+        public void endCDATA() throws SAXException {
+            delegate.endCDATA();
+        }
+
+        @Override
+        public void comment(char[] ch, int start, int length) throws 
SAXException {
+            delegate.comment(ch, start, length);
+        }
+
+        @Override
+        public void notationDecl(String name, String publicId, String 
systemId) throws SAXException {
+            delegate.notationDecl(name, publicId, systemId);
+        }
+
+        @Override
+        public void unparsedEntityDecl(String name, String publicId, String 
systemId, String notationName)
+                throws SAXException {
+            delegate.unparsedEntityDecl(name, publicId, systemId, 
notationName);
+        }
+
+        @Override
+        public void elementDecl(String name, String model) throws SAXException 
{
+            if (delegate instanceof DeclHandler) {
+                ((DeclHandler) delegate).elementDecl(name, model);
+            }
+        }
+
+        @Override
+        public void attributeDecl(String eName, String aName, String type, 
String mode, String value)
+                throws SAXException {
+            if (delegate instanceof DeclHandler) {
+                ((DeclHandler) delegate).attributeDecl(eName, aName, type, 
mode, value);
+            }
+        }
+
+        @Override
+        public void internalEntityDecl(String name, String value) throws 
SAXException {
+            if (delegate instanceof DeclHandler) {
+                ((DeclHandler) delegate).internalEntityDecl(name, value);
+            }
+        }
+
+        @Override
+        public void externalEntityDecl(String name, String publicId, String 
systemId) throws SAXException {
+            if (delegate instanceof DeclHandler) {
+                ((DeclHandler) delegate).externalEntityDecl(name, publicId, 
systemId);
+            }
+        }
+    }
+
+    /**
+     * Ensures {@link SecuredTransformer} is used for transformers obtained 
from compiled templates, which is
+     * how camel-xslt gets hold of them.
+     */
+    private static final class SecuredTemplates implements Templates {
+        private final Templates delegate;
+        private final XalanTransformerFactory factory;
+
+        SecuredTemplates(Templates delegate, XalanTransformerFactory factory) {
+            this.delegate = delegate;
+            this.factory = factory;
+        }
+
+        @Override
+        public Transformer newTransformer() throws 
TransformerConfigurationException {
+            return factory.secure(delegate.newTransformer());
+        }
+
+        @Override
+        public Properties getOutputProperties() {
+            return delegate.getOutputProperties();
+        }
     }
 
+    /**
+     * Applies {@link XalanTransformerFactory#secureInputSource(Source)} to 
the document being transformed.
+     */
+    private static final class SecuredTransformer extends Transformer {
+        private final Transformer delegate;
+        private final URIResolver restrictingUriResolver;
+
+        SecuredTransformer(Transformer delegate, URIResolver 
restrictingUriResolver) {
+            this.delegate = delegate;
+            this.restrictingUriResolver = restrictingUriResolver;
+        }
+
+        @Override
+        public void transform(Source xmlSource, javax.xml.transform.Result 
outputTarget) throws TransformerException {
+            delegate.transform(secureInputSource(xmlSource), outputTarget);
+        }
+
+        @Override
+        public void setParameter(String name, Object value) {
+            delegate.setParameter(name, value);
+        }
+
+        @Override
+        public Object getParameter(String name) {
+            return delegate.getParameter(name);
+        }
+
+        @Override
+        public void clearParameters() {
+            delegate.clearParameters();
+        }
+
+        /**
+         * Clearing the resolver leaves the JDK restricted, because it enforces
+         * {@link XMLConstants#ACCESS_EXTERNAL_STYLESHEET} independently of 
one. Here the resolver is the
+         * only means of enforcement, so a null falls back to the restriction 
rather than removing it.
+         */
+        @Override
+        public void setURIResolver(URIResolver resolver) {
+            delegate.setURIResolver(resolver == null ? restrictingUriResolver 
: resolver);
+        }
+
+        @Override
+        public URIResolver getURIResolver() {
+            return delegate.getURIResolver();
+        }
+
+        @Override
+        public void setOutputProperties(Properties oformat) {
+            delegate.setOutputProperties(oformat);
+        }
+
+        @Override
+        public Properties getOutputProperties() {
+            return delegate.getOutputProperties();
+        }
+
+        @Override
+        public void setOutputProperty(String name, String value) {
+            delegate.setOutputProperty(name, value);
+        }
+
+        @Override
+        public String getOutputProperty(String name) {
+            return delegate.getOutputProperty(name);
+        }
+
+        @Override
+        public void setErrorListener(ErrorListener listener) {
+            delegate.setErrorListener(listener);
+        }
+
+        @Override
+        public ErrorListener getErrorListener() {
+            return delegate.getErrorListener();
+        }
+
+        /**
+         * {@link Transformer#reset()} restores the configuration the 
transformer was created with, which
+         * for Xalan means dropping the {@link URIResolver} {@link 
#secure(Transformer)} installed. The
+         * restriction is not part of what a caller is resetting, so it is put 
back.
+         */
+        @Override
+        public void reset() {
+            delegate.reset();
+            delegate.setURIResolver(restrictingUriResolver);
+        }
+    }
 }
diff --git a/extensions/xslt/runtime/src/main/doc/configuration.adoc 
b/extensions/xslt/runtime/src/main/doc/configuration.adoc
index d3ed2a04b4..2cf56b4ddc 100644
--- a/extensions/xslt/runtime/src/main/doc/configuration.adoc
+++ b/extensions/xslt/runtime/src/main/doc/configuration.adoc
@@ -34,6 +34,35 @@ TransformerFactory features can be configured using 
following property:
 ----
 
quarkus.camel.xslt.features."http\://javax.xml.XMLConstants/feature/secure-processing"=false
 ----
+
+Features are applied to every template the component transforms with, whether 
it was compiled to a translet at
+build time or loaded at runtime. A feature the `TransformerFactory` does not 
support fails endpoint creation.
+
+WARNING: Disabling secure-processing permits templates to call Xalan extension 
functions, and is logged as a
+warning on startup. Only do this where every template the application 
transforms with is trusted.
+
+=== External access
+The extension transforms with Xalan-J rather than the XSLT implementation 
built into the JDK, because translets
+have to be compiled ahead of time to work in native mode. Xalan-J 2.7.x 
predates JAXP 1.5 and cannot honour
+`javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD` or `ACCESS_EXTERNAL_STYLESHEET`, 
so `setAttribute` throws for both,
+and its secure-processing feature restricts extension functions without 
implying the external access
+restrictions the JDK applies under the same feature. The extension applies 
those restrictions itself:
+
+* Documents being transformed are parsed with an `XMLReader` that does not 
resolve external general entities, so
+a `SYSTEM` entity in a `DOCTYPE` declaration does not expand into the result. 
A `SAXSource` carrying an
+`XMLReader` the caller configured is used as it is.
+* Resources fetched at transform time by the `document()` function are denied 
unless a
+`javax.xml.transform.URIResolver` resolves them. This applies to every entry 
point that hands out something to
+transform with, so a `TransformerHandler` or an `XMLFilter` is restricted just 
as a `Transformer` is. The
+component installs a resolver on every transformer it uses, so routes resolve 
`document()` as they do on plain
+Camel.
+
+`<xsl:import>` and `<xsl:include>` are not restricted. Xalan dereferences 
those hrefs itself while compiling a
+stylesheet, ignoring the resolver it consulted first. Stylesheets are 
deployment owned rather than attacker
+controlled, and the component resolves includes through its own unrestricted 
resolver in any case.
+
+NOTE: This factory is registered as the JAXP default, so the restrictions 
above also apply to code in the
+application that obtains a `TransformerFactory` through 
`TransformerFactory.newInstance()`.
 === Extension functions support
 https://xml.apache.org/xalan-j/extensions.html[Xalan's extension functions]
 do work properly only when:
diff --git 
a/extensions/xslt/runtime/src/main/java/org/apache/camel/quarkus/component/xslt/CamelXsltRecorder.java
 
b/extensions/xslt/runtime/src/main/java/org/apache/camel/quarkus/component/xslt/CamelXsltRecorder.java
index 00965c9fcb..b74b211dd9 100644
--- 
a/extensions/xslt/runtime/src/main/java/org/apache/camel/quarkus/component/xslt/CamelXsltRecorder.java
+++ 
b/extensions/xslt/runtime/src/main/java/org/apache/camel/quarkus/component/xslt/CamelXsltRecorder.java
@@ -17,7 +17,9 @@
 package org.apache.camel.quarkus.component.xslt;
 
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
 
+import javax.xml.XMLConstants;
 import javax.xml.transform.TransformerException;
 import javax.xml.transform.TransformerFactory;
 import javax.xml.transform.URIResolver;
@@ -30,10 +32,17 @@ import 
org.apache.camel.component.xslt.TransformerFactoryConfigurationStrategy;
 import org.apache.camel.component.xslt.XsltComponent;
 import org.apache.camel.component.xslt.XsltEndpoint;
 import org.apache.camel.quarkus.support.xalan.XalanTransformerFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @Recorder
 public class CamelXsltRecorder {
 
+    private static final Logger LOG = 
LoggerFactory.getLogger(CamelXsltRecorder.class);
+
+    /** Guards the secure-processing warning so that it is logged once instead 
of per endpoint */
+    private static final AtomicBoolean SECURE_PROCESSING_WARNED = new 
AtomicBoolean();
+
     public RuntimeValue<XsltComponent> createXsltComponent(CamelXsltConfig 
config,
             RuntimeValue<RuntimeUriResolver.Builder> uriResolverBuilder) {
         final RuntimeUriResolver uriResolver = 
uriResolverBuilder.getValue().build();
@@ -91,17 +100,25 @@ public class CamelXsltRecorder {
 
         @Override
         public void configure(TransformerFactory tf, XsltEndpoint endpoint) {
-            final String className = 
uriResolver.getTransletClassName(endpoint.getResourceUri());
-            if (className != null) {
-                for (Map.Entry<String, Boolean> entry : features.entrySet()) {
-                    try {
-                        tf.setFeature(entry.getKey(), entry.getValue());
-                    } catch (TransformerException e) {
-                        throw new RuntimeException("Could not set 
TransformerFactory feature '"
-                                + entry.getKey() + "' = " + entry.getValue(), 
e);
-                    }
+            // The features are applied whether or not the template was 
compiled to a translet at build time,
+            // otherwise quarkus.camel.xslt.features would silently have no 
effect on runtime loaded templates.
+            for (Map.Entry<String, Boolean> entry : features.entrySet()) {
+                if 
(XMLConstants.FEATURE_SECURE_PROCESSING.equals(entry.getKey()) && 
!entry.getValue()
+                        && SECURE_PROCESSING_WARNED.compareAndSet(false, 
true)) {
+                    LOG.warn("Disabling {} via quarkus.camel.xslt.features 
allows XSLT templates to call"
+                            + " extension functions. Only do this if every 
template the application transforms with"
+                            + " is trusted.", 
XMLConstants.FEATURE_SECURE_PROCESSING);
                 }
+                try {
+                    tf.setFeature(entry.getKey(), entry.getValue());
+                } catch (TransformerException e) {
+                    throw new RuntimeException("Could not set 
TransformerFactory feature '"
+                            + entry.getKey() + "' = " + entry.getValue(), e);
+                }
+            }
 
+            final String className = 
uriResolver.getTransletClassName(endpoint.getResourceUri());
+            if (className != null) {
                 tf.setAttribute("use-classpath", true);
                 tf.setAttribute("translet-name", className);
                 tf.setAttribute("package-name", packageName);

Reply via email to