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