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

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


The following commit(s) were added to refs/heads/main by this push:
     new e65fd915f6fd CAMEL-24451: camel-xslt - honor 
ACCESS_EXTERNAL_STYLESHEET for runtime document()
e65fd915f6fd is described below

commit e65fd915f6fd0be99da1186216a79a65104149cb
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Sep 28 21:30:08 2026 +0200

    CAMEL-24451: camel-xslt - honor ACCESS_EXTERNAL_STYLESHEET for runtime 
document()
    
    Camel's default XSLT transformer factory sets ACCESS_EXTERNAL_STYLESHEET to
    deny-all, but JAXP only enforces it when no URIResolver returns a Source,
    and the xslt component always installs its own XsltUriResolver. So an
    external http:, https:, ftp: or file: reference in document() was
    resolved at transform time regardless, and an untrusted header bound as a
    stylesheet parameter could make the transform read an attacker-chosen
    resource.
    
    The resolver installed on the transformer now honours the factory's
    ACCESS_EXTERNAL_STYLESHEET: a denied external reference throws a
    TransformerException (logged as a warning), so the transform fails
    instead of reading it, matching plain JAXP. The protocol is compared
    case-insensitively. camel-xslt-saxon is affected the same way. Internal
    classpath:, ref: and bean: references, and xsl:include/xsl:import, are
    unaffected; a route that needs external document() access supplies a
    TransformerFactory that permits the protocols. The upgrade guide
    describes it.
    
    Closes #26904
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 .../apache/camel/catalog/docs/xslt-component.adoc  |  38 +++++++-
 .../camel-xslt/src/main/docs/xslt-component.adoc   |  38 +++++++-
 .../apache/camel/component/xslt/XsltBuilder.java   |  45 ++++++++-
 .../camel/component/xslt/XsltUriResolver.java      |  78 ++++++++++++++++
 .../xml/XsltUriResolverExternalAccessTest.java     |  96 +++++++++++++++++++
 .../xslt/XsltDocumentExternalAccessTest.java       | 103 +++++++++++++++++++++
 .../xslt/camel24451_document_external.xsl          |  27 ++++++
 .../xslt/camel24451_document_relative.xsl          |  27 ++++++
 .../component/xslt/camel24451_external_lookup.xml  |  22 +++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  36 +++++++
 10 files changed, 507 insertions(+), 3 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xslt-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xslt-component.adoc
index eaf816691199..cd1110bebd1e 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xslt-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xslt-component.adoc
@@ -220,10 +220,46 @@ available:
 <xsl: ...... >
 
    <xsl:param name="myParam"/>
-  
+
     <xsl:template ...>
 ----
 
+[WARNING]
+====
+The automatic binding adds *every* message header as a stylesheet parameter, 
including headers
+that arrived from outside the route (for example HTTP query parameters, or 
headers set by an
+upstream sender). A stylesheet that passes a parameter into the XPath 
`document()` function
+therefore lets an external sender influence which resource the transform reads.
+
+When the `xslt` endpoint is reachable from an untrusted source and the 
stylesheet is not meant to
+take parameters from the wire, strip the incoming headers before the `xslt` 
step, keeping only the
+parameters the stylesheet expects:
+
+[source,java]
+----
+from("jetty:http://0.0.0.0:8080/transform";)
+    // keep only the parameters the stylesheet declares; drop everything that 
arrived on the wire
+    .removeHeaders("*", "myParam")
+    .to("xslt:MyTransform.xsl");
+----
+
+Separately, Camel's default XSLT transformer factory already denies external 
resource access — it
+sets `ACCESS_EXTERNAL_DTD` and `ACCESS_EXTERNAL_STYLESHEET` to the empty 
(deny-all) value. Since
+Camel 4.23 the always-installed resolver honours that setting, so when 
`document()` resolves an
+external `http:`, `https:`, `ftp:` or `file:` URI at transform time the 
reference is refused by
+default: the transform fails with a document-retrieval error (and the reason 
is logged as a warning)
+instead of reading the resource. This includes a *relative* `document()` when 
the stylesheet itself
+was loaded from `file:` (it resolves to a `file:` URI beside the stylesheet); 
stylesheets loaded from
+`classpath:` are unaffected, since a relative `document()` there resolves to a 
`classpath:` URI. To
+permit specific external protocols, supply your own `TransformerFactory` 
(through the
+`transformerFactory` option) whose `ACCESS_EXTERNAL_STYLESHEET` allows them — 
set the attribute to a
+comma-separated protocol list (such as `file` or `http,https`) or to `all`. 
Camel's internal
+`classpath:`, `ref:` and `bean:` schemes are resource lookups outside the JAXP 
external-access model
+and remain available. A stylesheet's `xsl:include` / `xsl:import` references 
are resolved when the
+stylesheet is compiled (they are part of the route definition) and are not 
affected by this runtime
+check.
+====
+
 == Spring XML versions
 
 To use the above examples in Spring XML, you would use something like the 
following code:
diff --git a/components/camel-xslt/src/main/docs/xslt-component.adoc 
b/components/camel-xslt/src/main/docs/xslt-component.adoc
index eaf816691199..cd1110bebd1e 100644
--- a/components/camel-xslt/src/main/docs/xslt-component.adoc
+++ b/components/camel-xslt/src/main/docs/xslt-component.adoc
@@ -220,10 +220,46 @@ available:
 <xsl: ...... >
 
    <xsl:param name="myParam"/>
-  
+
     <xsl:template ...>
 ----
 
+[WARNING]
+====
+The automatic binding adds *every* message header as a stylesheet parameter, 
including headers
+that arrived from outside the route (for example HTTP query parameters, or 
headers set by an
+upstream sender). A stylesheet that passes a parameter into the XPath 
`document()` function
+therefore lets an external sender influence which resource the transform reads.
+
+When the `xslt` endpoint is reachable from an untrusted source and the 
stylesheet is not meant to
+take parameters from the wire, strip the incoming headers before the `xslt` 
step, keeping only the
+parameters the stylesheet expects:
+
+[source,java]
+----
+from("jetty:http://0.0.0.0:8080/transform";)
+    // keep only the parameters the stylesheet declares; drop everything that 
arrived on the wire
+    .removeHeaders("*", "myParam")
+    .to("xslt:MyTransform.xsl");
+----
+
+Separately, Camel's default XSLT transformer factory already denies external 
resource access — it
+sets `ACCESS_EXTERNAL_DTD` and `ACCESS_EXTERNAL_STYLESHEET` to the empty 
(deny-all) value. Since
+Camel 4.23 the always-installed resolver honours that setting, so when 
`document()` resolves an
+external `http:`, `https:`, `ftp:` or `file:` URI at transform time the 
reference is refused by
+default: the transform fails with a document-retrieval error (and the reason 
is logged as a warning)
+instead of reading the resource. This includes a *relative* `document()` when 
the stylesheet itself
+was loaded from `file:` (it resolves to a `file:` URI beside the stylesheet); 
stylesheets loaded from
+`classpath:` are unaffected, since a relative `document()` there resolves to a 
`classpath:` URI. To
+permit specific external protocols, supply your own `TransformerFactory` 
(through the
+`transformerFactory` option) whose `ACCESS_EXTERNAL_STYLESHEET` allows them — 
set the attribute to a
+comma-separated protocol list (such as `file` or `http,https`) or to `all`. 
Camel's internal
+`classpath:`, `ref:` and `bean:` schemes are resource lookups outside the JAXP 
external-access model
+and remain available. A stylesheet's `xsl:include` / `xsl:import` references 
are resolved when the
+stylesheet is compiled (they are part of the route definition) and are not 
affected by this runtime
+check.
+====
+
 == Spring XML versions
 
 To use the above examples in Spring XML, you would use something like the 
following code:
diff --git 
a/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltBuilder.java
 
b/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltBuilder.java
index aa0fe1653ea8..68b0cad97afc 100644
--- 
a/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltBuilder.java
+++ 
b/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltBuilder.java
@@ -29,6 +29,7 @@ import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
+import javax.xml.XMLConstants;
 import javax.xml.transform.ErrorListener;
 import javax.xml.transform.Result;
 import javax.xml.transform.Source;
@@ -73,6 +74,9 @@ public class XsltBuilder implements Processor {
     private ResultHandlerFactory resultHandlerFactory = new 
StringResultHandlerFactory();
     private boolean failOnNullBody = true;
     private URIResolver uriResolver;
+    // the resolver installed on the transformer for runtime document() 
resolution; enforces the factory's
+    // ACCESS_EXTERNAL_STYLESHEET restriction while stylesheet compilation 
(xsl:include/import) stays unrestricted
+    private volatile URIResolver runtimeUriResolver;
     private boolean deleteOutputFile;
     private ErrorListener errorListener;
     private EntityResolver entityResolver;
@@ -403,6 +407,8 @@ public class XsltBuilder implements Processor {
 
     public void setUriResolver(URIResolver uriResolver) {
         this.uriResolver = uriResolver;
+        // drop any cached runtime resolver derived from the previous one
+        this.runtimeUriResolver = null;
     }
 
     public void setEntityResolver(EntityResolver entityResolver) {
@@ -479,7 +485,7 @@ public class XsltBuilder implements Processor {
         if (uriResolver == null) {
             uriResolver = new XsltUriResolver(exchange.getContext(), null);
         }
-        transformer.setURIResolver(uriResolver);
+        transformer.setURIResolver(resolveRuntimeUriResolver());
         if (errorListener == null) {
             // set our error listener, so we can capture errors and report 
them back on the exchange
             transformer.setErrorListener(new 
DefaultTransformErrorHandler(exchange));
@@ -498,6 +504,43 @@ public class XsltBuilder implements Processor {
         transformer.setParameter("out", exchange.getOut());
     }
 
+    /**
+     * Resolves the resolver to install on the transformer for runtime {@code 
document()} resolution. When Camel's own
+     * {@link XsltUriResolver} is in use and the transformer factory restricts 
external stylesheet access, a restricted
+     * copy is installed so {@code document()} honours {@code 
ACCESS_EXTERNAL_STYLESHEET}. Stylesheet compilation
+     * ({@code xsl:include} / {@code xsl:import}) keeps using the unrestricted 
resolver on the factory, so a route
+     * author's own includes are unaffected. The result is cached as the 
factory configuration is fixed once the builder
+     * is initialized.
+     */
+    private URIResolver resolveRuntimeUriResolver() {
+        URIResolver runtime = runtimeUriResolver;
+        if (runtime == null) {
+            runtime = uriResolver;
+            if (uriResolver instanceof XsltUriResolver xsltUriResolver) {
+                Set<String> allowedExternalProtocols = 
resolveAllowedExternalProtocols();
+                if (allowedExternalProtocols != null) {
+                    runtime = 
xsltUriResolver.withAllowedExternalProtocols(allowedExternalProtocols);
+                }
+            }
+            runtimeUriResolver = runtime;
+        }
+        return runtime;
+    }
+
+    /**
+     * Reads the transformer factory's {@code ACCESS_EXTERNAL_STYLESHEET} 
attribute as the set of external protocols the
+     * runtime resolver may load. Returns {@code null} (unrestricted) when the 
attribute is unset, {@code "all"}, or not
+     * supported by the factory.
+     */
+    private Set<String> resolveAllowedExternalProtocols() {
+        try {
+            Object value = 
converter.getTransformerFactory().getAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET);
+            return XsltUriResolver.parseAllowedProtocols(value != null ? 
value.toString() : null);
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+    }
+
     protected void addParameters(Transformer transformer, Map<String, Object> 
map) {
         Set<Map.Entry<String, Object>> propertyEntries = map.entrySet();
         for (Map.Entry<String, Object> entry : propertyEntries) {
diff --git 
a/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltUriResolver.java
 
b/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltUriResolver.java
index 927952617fdd..22f24cf077d1 100644
--- 
a/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltUriResolver.java
+++ 
b/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltUriResolver.java
@@ -18,6 +18,9 @@ package org.apache.camel.component.xslt;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
 
 import javax.xml.transform.Source;
 import javax.xml.transform.TransformerException;
@@ -44,13 +47,28 @@ public class XsltUriResolver implements URIResolver {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(XsltUriResolver.class);
 
+    // protocols governed by the JAXP ACCESS_EXTERNAL_STYLESHEET attribute; 
Camel's classpath:, ref: and bean: are
+    // internal resource schemes outside the JAXP external-access model and 
are always resolved
+    private static final Set<String> EXTERNAL_PROTOCOLS = Set.of("http", 
"https", "ftp", "file");
+
     private final CamelContext context;
     private final String location;
     private final String baseScheme;
+    private final Set<String> allowedExternalProtocols;
 
     public XsltUriResolver(CamelContext context, String location) {
+        this(context, location, null);
+    }
+
+    /**
+     * @param allowedExternalProtocols the external protocols this resolver 
may load, mirroring the factory's
+     *                                 {@code ACCESS_EXTERNAL_STYLESHEET}; 
{@code null} leaves external access
+     *                                 unrestricted (the default), an empty 
set forbids every external protocol
+     */
+    public XsltUriResolver(CamelContext context, String location, Set<String> 
allowedExternalProtocols) {
         this.context = context;
         this.location = location;
+        this.allowedExternalProtocols = allowedExternalProtocols;
         if (ResourceHelper.hasScheme(location)) {
             baseScheme = ResourceHelper.getScheme(location);
         } else {
@@ -59,6 +77,34 @@ public class XsltUriResolver implements URIResolver {
         }
     }
 
+    /**
+     * Returns a copy of this resolver that additionally enforces the given 
external-protocol allow-list, preserving the
+     * {@link CamelContext} and location so relative resolution is unchanged. 
Used to install a restricted resolver at
+     * transform time (for {@code document()}) while leaving stylesheet 
compilation unrestricted.
+     */
+    public XsltUriResolver withAllowedExternalProtocols(Set<String> 
allowedExternalProtocols) {
+        return new XsltUriResolver(context, location, 
allowedExternalProtocols);
+    }
+
+    /**
+     * Parses a JAXP {@code ACCESS_EXTERNAL_*} attribute value into the set of 
allowed protocols, or {@code null} when
+     * access is unrestricted. The value is a comma-separated protocol list; 
{@code "all"} means unrestricted and an
+     * empty string forbids every external protocol.
+     */
+    public static Set<String> parseAllowedProtocols(String 
accessExternalValue) {
+        if (accessExternalValue == null || 
"all".equals(accessExternalValue.trim())) {
+            return null;
+        }
+        Set<String> protocols = new HashSet<>();
+        for (String protocol : accessExternalValue.split(",")) {
+            String trimmed = protocol.trim();
+            if (!trimmed.isEmpty()) {
+                protocols.add(trimmed);
+            }
+        }
+        return protocols;
+    }
+
     @Override
     public Source resolve(String href, String base) throws 
TransformerException {
         // supports the empty href
@@ -74,6 +120,7 @@ public class XsltUriResolver implements URIResolver {
         String scheme = ResourceHelper.getScheme(href);
 
         if (scheme != null) {
+            checkExternalAccessAllowed(href, scheme);
             // need to compact paths for file/classpath as it can be relative 
paths using .. to go backwards
             String hrefPath = StringHelper.after(href, scheme);
             if ("file:".equals(scheme)) {
@@ -115,4 +162,35 @@ public class XsltUriResolver implements URIResolver {
         }
     }
 
+    /**
+     * Enforces the configured {@code ACCESS_EXTERNAL_STYLESHEET} restriction 
by throwing a {@link TransformerException}
+     * for a forbidden external protocol. JAXP applies that attribute only 
when no custom {@link URIResolver} returns a
+     * {@link Source}, and Camel always installs this resolver, so it must 
apply the same limit itself. Throwing matches
+     * plain JAXP behaviour: the processor reports an access error rather than 
silently reading the resource (the JDK's
+     * XSLTC turns a resolver exception into a document retrieval failure and 
does not fall back to reading it). Only
+     * the standard external protocols are governed; Camel's {@code 
classpath:}, {@code ref:} and {@code bean:} schemes
+     * are internal lookups outside the JAXP model and are always resolved. 
Does nothing when external access is
+     * unrestricted ({@code allowedExternalProtocols == null}).
+     */
+    private void checkExternalAccessAllowed(String href, String scheme) throws 
TransformerException {
+        if (allowedExternalProtocols == null) {
+            return;
+        }
+        // scheme carries a trailing ':' (e.g. "http:"); compare 
case-insensitively so an upper/mixed-case scheme
+        // cannot slip past the guard
+        String protocol = scheme.endsWith(":") ? scheme.substring(0, 
scheme.length() - 1) : scheme;
+        protocol = protocol.toLowerCase(Locale.ROOT);
+        if (EXTERNAL_PROTOCOLS.contains(protocol) && 
!allowedExternalProtocols.contains(protocol)) {
+            // log the denial explicitly: the XSLT processor turns this 
exception into a generic document retrieval
+            // failure, so this WARN is what actually tells an operator why 
document() did not read the resource
+            LOG.warn("Refusing to resolve external resource {} for the XSLT 
document() function: protocol '{}' is not"
+                     + " permitted by the transformer factory's 
ACCESS_EXTERNAL_STYLESHEET restriction",
+                    href, protocol);
+            throw new TransformerException(
+                    "Refusing to resolve external resource " + href + " for 
the XSLT document() function: protocol '"
+                                           + protocol
+                                           + "' is not permitted by the 
transformer factory's ACCESS_EXTERNAL_STYLESHEET restriction");
+        }
+    }
+
 }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/builder/xml/XsltUriResolverExternalAccessTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/builder/xml/XsltUriResolverExternalAccessTest.java
new file mode 100644
index 000000000000..c01d96e12af8
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/builder/xml/XsltUriResolverExternalAccessTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.builder.xml;
+
+import java.util.Set;
+
+import javax.xml.transform.TransformerException;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.component.xslt.XsltUriResolver;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * {@link XsltUriResolver} honours a factory's {@code 
ACCESS_EXTERNAL_STYLESHEET} restriction for the standard external
+ * protocols by throwing a {@link TransformerException} (matching plain JAXP, 
which reports an access error).
+ * Camel-internal schemes (classpath, ref, bean) are outside the JAXP 
external-access model and stay resolvable
+ * (CAMEL-24451).
+ */
+public class XsltUriResolverExternalAccessTest extends ContextTestSupport {
+
+    @Test
+    public void externalProtocolIsRefusedWhenAccessIsRestricted() {
+        // ACCESS_EXTERNAL_STYLESHEET="" -> no external protocol permitted (a 
non-existent file so nothing is read)
+        XsltUriResolver resolver = new XsltUriResolver(context, null, 
XsltUriResolver.parseAllowedProtocols(""));
+        TransformerException e = assertThrows(TransformerException.class,
+                () -> resolver.resolve("file:/does-not-exist-camel-24451.xsl", 
null));
+        assertTrue(e.getMessage().contains("ACCESS_EXTERNAL_STYLESHEET"), 
"unexpected message: " + e.getMessage());
+    }
+
+    @Test
+    public void aProtocolOutsideTheAllowedListIsRefused() {
+        // only http permitted, so a file: reference is refused with the 
access-denied message
+        XsltUriResolver resolver = new XsltUriResolver(context, null, 
XsltUriResolver.parseAllowedProtocols("http"));
+        TransformerException e = assertThrows(TransformerException.class,
+                () -> resolver.resolve("file:/does-not-exist-camel-24451.xsl", 
null));
+        assertTrue(e.getMessage().contains("ACCESS_EXTERNAL_STYLESHEET"), 
"unexpected message: " + e.getMessage());
+    }
+
+    @Test
+    public void anAllowedProtocolIsResolvedNormally() {
+        // file permitted -> the guard does not intervene, so resolution is 
attempted and fails only because the file is
+        // absent (the failure is not the access-denied message)
+        XsltUriResolver resolver = new XsltUriResolver(context, null, 
XsltUriResolver.parseAllowedProtocols("file"));
+        TransformerException e = assertThrows(TransformerException.class,
+                () -> resolver.resolve("file:/does-not-exist-camel-24451.xsl", 
null));
+        assertTrue(!e.getMessage().contains("ACCESS_EXTERNAL_STYLESHEET"), 
"unexpected access denial: " + e.getMessage());
+    }
+
+    @Test
+    public void camelInternalSchemesAreNotGoverned() {
+        // even with all external protocols denied, classpath: is a 
Camel-internal scheme, so the access check does not
+        // apply; resolution proceeds and fails only because the resource is 
absent
+        XsltUriResolver resolver = new XsltUriResolver(context, null, 
XsltUriResolver.parseAllowedProtocols(""));
+        TransformerException e = assertThrows(TransformerException.class,
+                () -> 
resolver.resolve("classpath:does-not-exist-camel-24451.xsl", null));
+        assertTrue(!e.getMessage().contains("ACCESS_EXTERNAL_STYLESHEET"),
+                "classpath must not be access-governed: " + e.getMessage());
+    }
+
+    @Test
+    public void unrestrictedAccessDoesNotRefuseExternalProtocols() {
+        // null (ACCESS_EXTERNAL_STYLESHEET unset or "all") -> no access 
restriction, so a failure is resolution, not an
+        // access denial
+        XsltUriResolver resolver = new XsltUriResolver(context, null, null);
+        TransformerException e = assertThrows(TransformerException.class,
+                () -> resolver.resolve("file:/does-not-exist-camel-24451.xsl", 
null));
+        assertTrue(!e.getMessage().contains("ACCESS_EXTERNAL_STYLESHEET"), 
"unexpected access denial: " + e.getMessage());
+    }
+
+    @Test
+    public void parseAllowedProtocols() {
+        assertNull(XsltUriResolver.parseAllowedProtocols("all"));
+        assertNull(XsltUriResolver.parseAllowedProtocols(null));
+        assertEquals(Set.of(), XsltUriResolver.parseAllowedProtocols(""));
+        assertEquals(Set.of("file", "http"), 
XsltUriResolver.parseAllowedProtocols("file, http"));
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/xslt/XsltDocumentExternalAccessTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/xslt/XsltDocumentExternalAccessTest.java
new file mode 100644
index 000000000000..968f124cdb05
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/xslt/XsltDocumentExternalAccessTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.component.xslt;
+
+import java.io.File;
+
+import javax.xml.XMLConstants;
+import javax.xml.transform.TransformerFactory;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.support.builder.xml.XMLConverterHelper;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The always-installed {@link XsltUriResolver} now honours the transformer 
factory's {@code ACCESS_EXTERNAL_STYLESHEET}
+ * restriction for the runtime {@code document()} function. Because Camel's 
default factory sets it to deny-all, an
+ * external resource referenced through {@code document()} is refused by 
default (the transform fails with an access
+ * error); relaxing the factory lets it through again (CAMEL-24451).
+ */
+public class XsltDocumentExternalAccessTest extends ContextTestSupport {
+
+    private static final String XSL = 
"org/apache/camel/component/xslt/camel24451_document_external.xsl";
+    // a file: stylesheet whose relative document() reference resolves to a 
file: URI beside it
+    private static final String XSL_FILE_RELATIVE
+            = 
"file:src/test/resources/org/apache/camel/component/xslt/camel24451_document_relative.xsl";
+    private static final String MARKER = "EXTERNAL-DATA-CAMEL-24451";
+
+    // an absolute file: URI to an existing resource - what an 
attacker-controlled header could point document() at
+    private static String externalUri() {
+        return new 
File("src/test/resources/org/apache/camel/component/xslt/camel24451_external_lookup.xml")
+                .getAbsoluteFile().toURI().toString();
+    }
+
+    /**
+     * Registers a factory that keeps Camel's hardening but permits the {@code 
file} protocol for external
+     * stylesheet/document access, mirroring what an operator would configure 
to opt back in.
+     */
+    @Override
+    protected Registry createCamelRegistry() throws Exception {
+        Registry registry = super.createCamelRegistry();
+        TransformerFactory factory = new 
XMLConverterHelper().createTransformerFactory();
+        factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "file");
+        registry.bind("relaxedFactory", factory);
+        return registry;
+    }
+
+    @Test
+    public void externalDocumentIsDeniedByDefault() {
+        // the default factory denies external access ("" == deny-all), so 
document() over an absolute file: URI (bound
+        // from a message header) is refused: the transform fails instead of 
reading the (existing) file. The resolver's
+        // TransformerException is turned into a document retrieval failure by 
the XSLT processor, so we assert the
+        // transform fails rather than on the message text (the access reason 
is logged as a WARN)
+        assertThrows(CamelExecutionException.class,
+                () -> template.requestBodyAndHeader("direct:default", "<a/>", 
"externalUri", externalUri()));
+    }
+
+    @Test
+    public void relativeDocumentInAFileStylesheetIsDeniedByDefault() {
+        // a stylesheet loaded from file: whose relative document() resolves 
to a file: URI beside it is also refused by
+        // default (the route author must relax the factory to allow it) - 
documented in the upgrade guide
+        assertThrows(CamelExecutionException.class,
+                () -> template.requestBody("direct:fileRelative", "<a/>"));
+    }
+
+    @Test
+    public void externalDocumentIsAllowedWhenTheFactoryPermitsFile() {
+        // a factory whose ACCESS_EXTERNAL_STYLESHEET permits file lets 
document() read the external resource again
+        String body = template.requestBodyAndHeader("direct:relaxed", "<a/>", 
"externalUri", externalUri(), String.class);
+        assertTrue(body.contains(MARKER), "expected the external document to 
be read, got: " + body);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:default").to("xslt:" + XSL);
+                from("direct:relaxed").to("xslt:" + XSL + 
"?transformerFactory=#relaxedFactory");
+                from("direct:fileRelative").to("xslt:" + XSL_FILE_RELATIVE);
+            }
+        };
+    }
+}
diff --git 
a/core/camel-core/src/test/resources/org/apache/camel/component/xslt/camel24451_document_external.xsl
 
b/core/camel-core/src/test/resources/org/apache/camel/component/xslt/camel24451_document_external.xsl
new file mode 100644
index 000000000000..5a3dcda2b26d
--- /dev/null
+++ 
b/core/camel-core/src/test/resources/org/apache/camel/component/xslt/camel24451_document_external.xsl
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform";>
+    <xsl:output method="xml" omit-xml-declaration="yes"/>
+    <!-- the external URI arrives as a parameter (bound from a message 
header): the CAMEL-24451 attack shape -->
+    <xsl:param name="externalUri"/>
+    <xsl:template match="/">
+        <result><xsl:value-of 
select="document($externalUri)/lookup/value"/></result>
+    </xsl:template>
+</xsl:stylesheet>
diff --git 
a/core/camel-core/src/test/resources/org/apache/camel/component/xslt/camel24451_document_relative.xsl
 
b/core/camel-core/src/test/resources/org/apache/camel/component/xslt/camel24451_document_relative.xsl
new file mode 100644
index 000000000000..ea1280dc2725
--- /dev/null
+++ 
b/core/camel-core/src/test/resources/org/apache/camel/component/xslt/camel24451_document_relative.xsl
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform";>
+    <xsl:output method="xml" omit-xml-declaration="yes"/>
+    <!-- a relative document() reference; when the stylesheet itself is loaded 
from file:, this resolves to a file: URI
+         relative to the stylesheet directory (CAMEL-24451) -->
+    <xsl:template match="/">
+        <result><xsl:value-of 
select="document('camel24451_external_lookup.xml')/lookup/value"/></result>
+    </xsl:template>
+</xsl:stylesheet>
diff --git 
a/core/camel-core/src/test/resources/org/apache/camel/component/xslt/camel24451_external_lookup.xml
 
b/core/camel-core/src/test/resources/org/apache/camel/component/xslt/camel24451_external_lookup.xml
new file mode 100644
index 000000000000..c712594dcb7e
--- /dev/null
+++ 
b/core/camel-core/src/test/resources/org/apache/camel/component/xslt/camel24451_external_lookup.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<lookup>
+    <value>EXTERNAL-DATA-CAMEL-24451</value>
+</lookup>
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 973341679cbd..473d491af122 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -3350,6 +3350,42 @@ Previously it read the `SessionID` header when the route 
completed, so a route t
 which session received the reply. A route that changed the `SessionID` header 
to send the reply to another session
 must now send that message with a QuickFIX/J producer endpoint that sets the 
`sessionID` option.
 
+=== camel-xslt / camel-xslt-saxon - external document() access is denied by 
default
+
+Camel's default XSLT transformer factory (`XMLConverterHelper`) sets 
`ACCESS_EXTERNAL_DTD` and
+`ACCESS_EXTERNAL_STYLESHEET` to the empty (deny-all) value. JAXP enforces 
`ACCESS_EXTERNAL_STYLESHEET`
+only when no `URIResolver` returns a `Source`, but the `xslt` component always 
installs its own
+`XsltUriResolver` on the transformer, so that resolver used to resolve 
external `http:`, `https:`,
+`ftp:` and `file:` references regardless — the factory's deny-all setting 
silently did nothing at
+transform time.
+
+The resolver installed on the transformer now honours the factory's 
`ACCESS_EXTERNAL_STYLESHEET`
+value. As a result, when a stylesheet's `document()` function resolves an 
external `http:`, `https:`,
+`ftp:` or `file:` URI at transform time, the reference is now **refused by 
default**: the resolver
+throws, so the transform fails with a document-retrieval error (and the reason 
is logged as a
+warning) instead of reading the resource. This matches what plain JAXP does 
with
+`ACCESS_EXTERNAL_STYLESHEET=""`, and closes a gap where an untrusted message 
header — bound as a
+stylesheet parameter and passed into `document()` — could make the transform 
read an attacker-chosen
+external resource.
+
+This also affects a **relative** `document()` when the stylesheet itself was 
loaded from `file:`
+(for example `xslt:file:/opt/xsl/t.xsl` with `document('codes.xml')`, which 
resolves to
+`file:/opt/xsl/codes.xml`): that is the route author's own lookup file, but it 
is an external `file:`
+reference and is denied by default (plain JAXP would deny it too). Stylesheets 
loaded from
+`classpath:` are unaffected, because a relative `document()` there resolves to 
a `classpath:` URI,
+which is outside the JAXP external-access model.
+
+`camel-xslt-saxon` is affected in the same way: `XsltSaxonEndpoint` sets the 
same deny-all attribute,
+Saxon reports it through `getAttribute`, and `XsltSaxonBuilder` extends 
`XsltBuilder`.
+
+If a route legitimately needs `document()` to read an external resource, 
supply a custom
+`TransformerFactory` via the `transformerFactory` option whose 
`ACCESS_EXTERNAL_STYLESHEET` permits
+the required protocols (set the attribute to a comma-separated protocol list 
such as `file` or
+`http,https`, or to `all`). Camel's internal `classpath:`, `ref:` and `bean:` 
schemes are outside the
+JAXP external-access model and remain resolvable. Stylesheet `xsl:include` / 
`xsl:import` references
+are resolved at compile time (they are part of the route definition, authored 
by the route author)
+and are unaffected by this change.
+
 == ThrottlingExceptionRoutePolicy
 
 `ThrottlingExceptionRoutePolicy.setKeepOpen(true)` now opens the circuit 
immediately and synchronously (the consumer

Reply via email to