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);