diff --git a/src/main/java/org/apache/commons/xml/DelegatingSAXParser.java b/src/main/java/org/apache/commons/xml/DelegatingSAXParser.java
deleted file mode 100644
index 26dc125..0000000
--- a/src/main/java/org/apache/commons/xml/DelegatingSAXParser.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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
- *
- * https://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.commons.xml;
-
-import javax.xml.parsers.SAXParser;
-import javax.xml.validation.Schema;
-
-import org.xml.sax.Parser;
-import org.xml.sax.SAXException;
-import org.xml.sax.SAXNotRecognizedException;
-import org.xml.sax.SAXNotSupportedException;
-import org.xml.sax.XMLReader;
-
-/**
- * {@link SAXParser} that forwards every method to a wrapped delegate.
- *
- *
The non-abstract {@code parse(...)} overloads inherited from {@link SAXParser} call {@code this.getXMLReader()} virtually, so a subclass that only
- * overrides {@link #getXMLReader()} can redirect every parse path through a different reader without having to override the overloads.
- */
-class DelegatingSAXParser extends SAXParser {
-
- private final SAXParser delegate;
-
- DelegatingSAXParser(final SAXParser delegate) {
- this.delegate = delegate;
- }
-
- @Override
- @SuppressWarnings("deprecation")
- public Parser getParser() throws SAXException {
- return delegate.getParser();
- }
-
- @Override
- public Object getProperty(final String name) throws SAXNotRecognizedException, SAXNotSupportedException {
- return delegate.getProperty(name);
- }
-
- @Override
- public Schema getSchema() {
- return delegate.getSchema();
- }
-
- @Override
- public XMLReader getXMLReader() throws SAXException {
- return delegate.getXMLReader();
- }
-
- @Override
- public boolean isNamespaceAware() {
- return delegate.isNamespaceAware();
- }
-
- @Override
- public boolean isValidating() {
- return delegate.isValidating();
- }
-
- @Override
- public boolean isXIncludeAware() {
- return delegate.isXIncludeAware();
- }
-
- @Override
- public void reset() {
- delegate.reset();
- }
-
- @Override
- public void setProperty(final String name, final Object value) throws SAXNotRecognizedException, SAXNotSupportedException {
- delegate.setProperty(name, value);
- }
-}
diff --git a/src/main/java/org/apache/commons/xml/DelegatingSAXParserFactory.java b/src/main/java/org/apache/commons/xml/DelegatingSAXParserFactory.java
deleted file mode 100644
index bb5323f..0000000
--- a/src/main/java/org/apache/commons/xml/DelegatingSAXParserFactory.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * 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
- *
- * https://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.commons.xml;
-
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.parsers.SAXParser;
-import javax.xml.parsers.SAXParserFactory;
-import javax.xml.validation.Schema;
-
-import org.xml.sax.SAXException;
-import org.xml.sax.SAXNotRecognizedException;
-import org.xml.sax.SAXNotSupportedException;
-
-/**
- * {@link SAXParserFactory} subclass that forwards every method to a wrapped delegate.
- */
-class DelegatingSAXParserFactory extends SAXParserFactory {
-
- private final SAXParserFactory delegate;
-
- DelegatingSAXParserFactory(final SAXParserFactory delegate) {
- this.delegate = delegate;
- }
-
- @Override
- public boolean getFeature(final String name) throws ParserConfigurationException, SAXNotRecognizedException, SAXNotSupportedException {
- return delegate.getFeature(name);
- }
-
- @Override
- public Schema getSchema() {
- return delegate.getSchema();
- }
-
- @Override
- public boolean isNamespaceAware() {
- return delegate.isNamespaceAware();
- }
-
- @Override
- public boolean isValidating() {
- return delegate.isValidating();
- }
-
- @Override
- public boolean isXIncludeAware() {
- return delegate.isXIncludeAware();
- }
-
- @Override
- public SAXParser newSAXParser() throws ParserConfigurationException, SAXException {
- return delegate.newSAXParser();
- }
-
- @Override
- public void setFeature(final String name, final boolean value) throws ParserConfigurationException, SAXNotRecognizedException, SAXNotSupportedException {
- delegate.setFeature(name, value);
- }
-
- @Override
- public void setNamespaceAware(final boolean awareness) {
- delegate.setNamespaceAware(awareness);
- }
-
- @Override
- public void setSchema(final Schema schema) {
- delegate.setSchema(schema);
- }
-
- @Override
- public void setValidating(final boolean validating) {
- delegate.setValidating(validating);
- }
-
- @Override
- public void setXIncludeAware(final boolean state) {
- delegate.setXIncludeAware(state);
- }
-}
diff --git a/src/main/java/org/apache/commons/xml/DelegatingSchemaFactory.java b/src/main/java/org/apache/commons/xml/DelegatingSchemaFactory.java
deleted file mode 100644
index 797d8ce..0000000
--- a/src/main/java/org/apache/commons/xml/DelegatingSchemaFactory.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * 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
- *
- * https://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.commons.xml;
-
-import javax.xml.transform.Source;
-import javax.xml.validation.Schema;
-import javax.xml.validation.SchemaFactory;
-
-import org.w3c.dom.ls.LSResourceResolver;
-import org.xml.sax.ErrorHandler;
-import org.xml.sax.SAXException;
-import org.xml.sax.SAXNotRecognizedException;
-import org.xml.sax.SAXNotSupportedException;
-
-/**
- * {@link SchemaFactory} subclass that forwards every method to a wrapped delegate.
- */
-class DelegatingSchemaFactory extends SchemaFactory {
-
- private final SchemaFactory delegate;
-
- DelegatingSchemaFactory(final SchemaFactory delegate) {
- this.delegate = delegate;
- }
-
- @Override
- public ErrorHandler getErrorHandler() {
- return delegate.getErrorHandler();
- }
-
- @Override
- public boolean getFeature(final String name) throws SAXNotRecognizedException, SAXNotSupportedException {
- return delegate.getFeature(name);
- }
-
- @Override
- public Object getProperty(final String name) throws SAXNotRecognizedException, SAXNotSupportedException {
- return delegate.getProperty(name);
- }
-
- @Override
- public LSResourceResolver getResourceResolver() {
- return delegate.getResourceResolver();
- }
-
- @Override
- public boolean isSchemaLanguageSupported(final String schemaLanguage) {
- return delegate.isSchemaLanguageSupported(schemaLanguage);
- }
-
- @Override
- public Schema newSchema() throws SAXException {
- return delegate.newSchema();
- }
-
- @Override
- public Schema newSchema(final Source[] schemas) throws SAXException {
- return delegate.newSchema(schemas);
- }
-
- @Override
- public void setErrorHandler(final ErrorHandler errorHandler) {
- delegate.setErrorHandler(errorHandler);
- }
-
- @Override
- public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException {
- delegate.setFeature(name, value);
- }
-
- @Override
- public void setProperty(final String name, final Object object) throws SAXNotRecognizedException, SAXNotSupportedException {
- delegate.setProperty(name, object);
- }
-
- @Override
- public void setResourceResolver(final LSResourceResolver resourceResolver) {
- delegate.setResourceResolver(resourceResolver);
- }
-}
diff --git a/src/main/java/org/apache/commons/xml/DelegatingTemplates.java b/src/main/java/org/apache/commons/xml/DelegatingTemplates.java
deleted file mode 100644
index f20d5a9..0000000
--- a/src/main/java/org/apache/commons/xml/DelegatingTemplates.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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
- *
- * https://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.commons.xml;
-
-import java.util.Properties;
-
-import javax.xml.transform.Templates;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerConfigurationException;
-
-/**
- * {@link Templates} wrapper that forwards every method to a wrapped delegate.
- */
-class DelegatingTemplates implements Templates {
-
- private final Templates delegate;
-
- DelegatingTemplates(final Templates delegate) {
- this.delegate = delegate;
- }
-
- @Override
- public Properties getOutputProperties() {
- return delegate.getOutputProperties();
- }
-
- @Override
- public Transformer newTransformer() throws TransformerConfigurationException {
- return delegate.newTransformer();
- }
-}
diff --git a/src/main/java/org/apache/commons/xml/DelegatingTransformer.java b/src/main/java/org/apache/commons/xml/DelegatingTransformer.java
deleted file mode 100644
index 189f3b0..0000000
--- a/src/main/java/org/apache/commons/xml/DelegatingTransformer.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * 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
- *
- * https://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.commons.xml;
-
-import java.util.Properties;
-
-import javax.xml.transform.ErrorListener;
-import javax.xml.transform.Result;
-import javax.xml.transform.Source;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerException;
-import javax.xml.transform.URIResolver;
-
-/**
- * {@link Transformer} subclass that forwards every method to a wrapped delegate.
- */
-class DelegatingTransformer extends Transformer {
-
- private final Transformer delegate;
-
- DelegatingTransformer(final Transformer delegate) {
- this.delegate = delegate;
- }
-
- @Override
- public void clearParameters() {
- delegate.clearParameters();
- }
-
- @Override
- public ErrorListener getErrorListener() {
- return delegate.getErrorListener();
- }
-
- @Override
- public Properties getOutputProperties() {
- return delegate.getOutputProperties();
- }
-
- @Override
- public String getOutputProperty(final String name) {
- return delegate.getOutputProperty(name);
- }
-
- @Override
- public Object getParameter(final String name) {
- return delegate.getParameter(name);
- }
-
- @Override
- public URIResolver getURIResolver() {
- return delegate.getURIResolver();
- }
-
- @Override
- public void reset() {
- delegate.reset();
- }
-
- @Override
- public void setErrorListener(final ErrorListener listener) {
- delegate.setErrorListener(listener);
- }
-
- @Override
- public void setOutputProperties(final Properties properties) {
- delegate.setOutputProperties(properties);
- }
-
- @Override
- public void setOutputProperty(final String name, final String value) {
- delegate.setOutputProperty(name, value);
- }
-
- @Override
- public void setParameter(final String name, final Object value) {
- delegate.setParameter(name, value);
- }
-
- @Override
- public void setURIResolver(final URIResolver resolver) {
- delegate.setURIResolver(resolver);
- }
-
- @Override
- public void transform(final Source xmlSource, final Result outputTarget) throws TransformerException {
- delegate.transform(xmlSource, outputTarget);
- }
-}
diff --git a/src/main/java/org/apache/commons/xml/DelegatingTransformerFactory.java b/src/main/java/org/apache/commons/xml/DelegatingTransformerFactory.java
deleted file mode 100644
index f067813..0000000
--- a/src/main/java/org/apache/commons/xml/DelegatingTransformerFactory.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * 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
- *
- * https://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.commons.xml;
-
-import javax.xml.transform.ErrorListener;
-import javax.xml.transform.Source;
-import javax.xml.transform.Templates;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerConfigurationException;
-import javax.xml.transform.URIResolver;
-import javax.xml.transform.sax.SAXTransformerFactory;
-import javax.xml.transform.sax.TemplatesHandler;
-import javax.xml.transform.sax.TransformerHandler;
-
-import org.xml.sax.XMLFilter;
-
-/**
- * {@link SAXTransformerFactory} subclass that forwards every method to a wrapped delegate.
- */
-class DelegatingTransformerFactory extends SAXTransformerFactory {
-
- private final SAXTransformerFactory delegate;
-
- DelegatingTransformerFactory(final SAXTransformerFactory delegate) {
- this.delegate = delegate;
- }
-
- @Override
- public Source getAssociatedStylesheet(final Source source, final String media, final String title, final String charset)
- throws TransformerConfigurationException {
- return delegate.getAssociatedStylesheet(source, media, title, charset);
- }
-
- @Override
- public Object getAttribute(final String name) {
- return delegate.getAttribute(name);
- }
-
- @Override
- public ErrorListener getErrorListener() {
- return delegate.getErrorListener();
- }
-
- @Override
- public boolean getFeature(final String name) {
- return delegate.getFeature(name);
- }
-
- @Override
- public URIResolver getURIResolver() {
- return delegate.getURIResolver();
- }
-
- @Override
- public Templates newTemplates(final Source source) throws TransformerConfigurationException {
- return delegate.newTemplates(source);
- }
-
- @Override
- public TemplatesHandler newTemplatesHandler() throws TransformerConfigurationException {
- return delegate.newTemplatesHandler();
- }
-
- @Override
- public Transformer newTransformer() throws TransformerConfigurationException {
- return delegate.newTransformer();
- }
-
- @Override
- public Transformer newTransformer(final Source source) throws TransformerConfigurationException {
- return delegate.newTransformer(source);
- }
-
- @Override
- public TransformerHandler newTransformerHandler() throws TransformerConfigurationException {
- return delegate.newTransformerHandler();
- }
-
- @Override
- public TransformerHandler newTransformerHandler(final Source source) throws TransformerConfigurationException {
- return delegate.newTransformerHandler(source);
- }
-
- @Override
- public TransformerHandler newTransformerHandler(final Templates templates) throws TransformerConfigurationException {
- return delegate.newTransformerHandler(templates);
- }
-
- @Override
- public XMLFilter newXMLFilter(final Source source) throws TransformerConfigurationException {
- return delegate.newXMLFilter(source);
- }
-
- @Override
- public XMLFilter newXMLFilter(final Templates templates) throws TransformerConfigurationException {
- return delegate.newXMLFilter(templates);
- }
-
- @Override
- public void setAttribute(final String name, final Object value) {
- delegate.setAttribute(name, value);
- }
-
- @Override
- public void setErrorListener(final ErrorListener listener) {
- delegate.setErrorListener(listener);
- }
-
- @Override
- public void setFeature(final String name, final boolean value) throws TransformerConfigurationException {
- delegate.setFeature(name, value);
- }
-
- @Override
- public void setURIResolver(final URIResolver resolver) {
- delegate.setURIResolver(resolver);
- }
-}
diff --git a/src/main/java/org/apache/commons/xml/DocumentBuilderHardener.java b/src/main/java/org/apache/commons/xml/DocumentBuilderHardener.java
index b0484a0..9aba89f 100644
--- a/src/main/java/org/apache/commons/xml/DocumentBuilderHardener.java
+++ b/src/main/java/org/apache/commons/xml/DocumentBuilderHardener.java
@@ -24,7 +24,6 @@
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.EntityResolver;
@@ -43,34 +42,11 @@
* {@code SecurityManager} as appropriate.
* {@code ACCESS_EXTERNAL_DTD}: the dividing capability. Implementations that honor it (the JDK-internal Xerces) block external fetches
* through the JAXP 1.5 properties and are returned as-is. Implementations that reject it (the external Xerces distribution) are wrapped so a deny-all
- * {@link EntityResolver} is installed on every {@link DocumentBuilder} produced.
+ * {@link EntityResolver} floor is installed on every {@link DocumentBuilder} produced.
*
*/
final class DocumentBuilderHardener {
- /**
- * Wrapper that sets a deny-all {@link EntityResolver} on every {@link DocumentBuilder} produced.
- *
- * Required for implementations that do not honor JAXP 1.5 {@code ACCESS_EXTERNAL_*} (the external Xerces distribution): the factory carries no resolver
- * of its own, so it has to be set on each builder.
- */
- private static final class HardeningDocumentBuilderFactory extends DelegatingDocumentBuilderFactory {
-
- private final EntityResolver resolver;
-
- HardeningDocumentBuilderFactory(final DocumentBuilderFactory delegate, final EntityResolver resolver) {
- super(delegate);
- this.resolver = resolver;
- }
-
- @Override
- public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
- final DocumentBuilder builder = super.newDocumentBuilder();
- builder.setEntityResolver(resolver);
- return builder;
- }
- }
-
/** Class name of Android's Harmony-based {@link DocumentBuilderFactory}, which exposes no hardening surface. */
private static final String ANDROID_DOCUMENT_BUILDER_FACTORY = "org.apache.harmony.xml.parsers.DocumentBuilderFactoryImpl";
@@ -95,7 +71,7 @@ && trySetAttribute(factory, XMLConstants.ACCESS_EXTERNAL_SCHEMA, "")) {
return factory;
}
// Rejected: external Xerces ignores ACCESS_EXTERNAL_*; install a deny-all resolver on every DocumentBuilder.
- return new HardeningDocumentBuilderFactory(factory, Resolvers.DenyAll.ENTITY2);
+ return new HardeningDocumentBuilderFactory(factory);
}
private DocumentBuilderHardener() {
diff --git a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilder.java b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilder.java
new file mode 100644
index 0000000..bb0b6a8
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilder.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
+ *
+ * https://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.commons.xml;
+
+import java.io.IOException;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.validation.Schema;
+
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+/**
+ * {@link DocumentBuilder} wrapper that keeps a deny-all {@link EntityResolver} as a non-overridable floor.
+ *
+ * A caller-set resolver is sandwiched inside a {@link Resolvers.FallbackDenyResolver} instead of replacing the deny-all one, so an external lookup the
+ * caller's resolver does not satisfy is denied rather than fetched. {@link #reset()} re-establishes the bare deny-all floor, matching the just-constructed
+ * state.
+ */
+final class HardeningDocumentBuilder extends DocumentBuilder {
+
+ private final DocumentBuilder delegate;
+
+ private final Resolvers.FallbackDenyResolver floor = new Resolvers.FallbackDenyResolver(null);
+
+ HardeningDocumentBuilder(final DocumentBuilder delegate) {
+ this.delegate = delegate;
+ delegate.setEntityResolver(floor);
+ }
+
+ @Override
+ public void setEntityResolver(final EntityResolver resolver) {
+ floor.setDelegate(resolver);
+ }
+
+ @Override
+ public void reset() {
+ delegate.reset();
+ floor.setDelegate(null);
+ delegate.setEntityResolver(floor);
+ }
+
+ //
+ @Override
+ public Document parse(final InputSource is) throws SAXException, IOException {
+ return delegate.parse(is);
+ }
+
+ @Override
+ public boolean isNamespaceAware() {
+ return delegate.isNamespaceAware();
+ }
+
+ @Override
+ public boolean isValidating() {
+ return delegate.isValidating();
+ }
+
+ @Override
+ public boolean isXIncludeAware() {
+ return delegate.isXIncludeAware();
+ }
+
+ @Override
+ public void setErrorHandler(final ErrorHandler eh) {
+ delegate.setErrorHandler(eh);
+ }
+
+ @Override
+ public Document newDocument() {
+ return delegate.newDocument();
+ }
+
+ @Override
+ public DOMImplementation getDOMImplementation() {
+ return delegate.getDOMImplementation();
+ }
+
+ @Override
+ public Schema getSchema() {
+ return delegate.getSchema();
+ }
+ //
+}
diff --git a/src/main/java/org/apache/commons/xml/DelegatingDocumentBuilderFactory.java b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java
similarity index 80%
rename from src/main/java/org/apache/commons/xml/DelegatingDocumentBuilderFactory.java
rename to src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java
index d641f87..35b33bb 100644
--- a/src/main/java/org/apache/commons/xml/DelegatingDocumentBuilderFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java
@@ -22,17 +22,29 @@
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.validation.Schema;
+import org.xml.sax.EntityResolver;
+
/**
- * {@link DocumentBuilderFactory} subclass that forwards every method to a wrapped delegate.
+ * {@link DocumentBuilderFactory} wrapper that keeps a deny-all {@link EntityResolver} floor on every {@link DocumentBuilder} produced.
+ *
+ * Wraps each produced builder in a {@link HardeningDocumentBuilder}; required when the underlying factory carries no resolver of its own and does not honor
+ * JAXP 1.5 {@code ACCESS_EXTERNAL_*} (e.g. the external Xerces distribution). A caller-set resolver is routed through the floor rather than replacing it. Kept
+ * as a standalone wrapper so any hardener can reuse the floor.
*/
-class DelegatingDocumentBuilderFactory extends DocumentBuilderFactory {
+final class HardeningDocumentBuilderFactory extends DocumentBuilderFactory {
private final DocumentBuilderFactory delegate;
- DelegatingDocumentBuilderFactory(final DocumentBuilderFactory delegate) {
+ HardeningDocumentBuilderFactory(final DocumentBuilderFactory delegate) {
this.delegate = delegate;
}
+ @Override
+ public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
+ return new HardeningDocumentBuilder(delegate.newDocumentBuilder());
+ }
+
+ //
@Override
public Object getAttribute(final String name) {
return delegate.getAttribute(name);
@@ -83,11 +95,6 @@ public boolean isXIncludeAware() {
return delegate.isXIncludeAware();
}
- @Override
- public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
- return delegate.newDocumentBuilder();
- }
-
@Override
public void setAttribute(final String name, final Object value) {
delegate.setAttribute(name, value);
@@ -137,4 +144,5 @@ public void setValidating(final boolean validating) {
public void setXIncludeAware(final boolean state) {
delegate.setXIncludeAware(state);
}
+ //
}
diff --git a/src/main/java/org/apache/commons/xml/HardeningSAXParser.java b/src/main/java/org/apache/commons/xml/HardeningSAXParser.java
index 766c399..d8bed29 100644
--- a/src/main/java/org/apache/commons/xml/HardeningSAXParser.java
+++ b/src/main/java/org/apache/commons/xml/HardeningSAXParser.java
@@ -18,9 +18,12 @@
package org.apache.commons.xml;
import javax.xml.parsers.SAXParser;
+import javax.xml.validation.Schema;
import org.xml.sax.Parser;
import org.xml.sax.SAXException;
+import org.xml.sax.SAXNotRecognizedException;
+import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderAdapter;
@@ -35,19 +38,21 @@
* every parse must run through the same instance. The {@code parse(...)} overloads inherited from {@link SAXParser} dispatch virtually to {@link #getXMLReader()}
* and {@link #getParser()}, so they too run through the hardened views without further overrides.
*/
-final class HardeningSAXParser extends DelegatingSAXParser {
+final class HardeningSAXParser extends SAXParser {
+
+ private final SAXParser delegate;
private XMLReader hardenedReader;
private Parser hardenedParser;
HardeningSAXParser(final SAXParser delegate) {
- super(delegate);
+ this.delegate = delegate;
}
@Override
public XMLReader getXMLReader() throws SAXException {
if (hardenedReader == null) {
- hardenedReader = SAXParserHardener.hardenReader(super.getXMLReader());
+ hardenedReader = SAXParserHardener.hardenReader(delegate.getXMLReader());
}
return hardenedReader;
}
@@ -62,4 +67,41 @@ public Parser getParser() throws SAXException {
}
return hardenedParser;
}
+
+ //
+ @Override
+ public Object getProperty(final String name) throws SAXNotRecognizedException, SAXNotSupportedException {
+ return delegate.getProperty(name);
+ }
+
+ @Override
+ public Schema getSchema() {
+ return delegate.getSchema();
+ }
+
+ @Override
+ public boolean isNamespaceAware() {
+ return delegate.isNamespaceAware();
+ }
+
+ @Override
+ public boolean isValidating() {
+ return delegate.isValidating();
+ }
+
+ @Override
+ public boolean isXIncludeAware() {
+ return delegate.isXIncludeAware();
+ }
+
+ @Override
+ public void reset() {
+ delegate.reset();
+ }
+
+ @Override
+ public void setProperty(final String name, final Object value) throws SAXNotRecognizedException, SAXNotSupportedException {
+ delegate.setProperty(name, value);
+ }
+ //
}
diff --git a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java
index df78abe..e0ea507 100644
--- a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java
@@ -20,8 +20,11 @@
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
+import javax.xml.validation.Schema;
import org.xml.sax.SAXException;
+import org.xml.sax.SAXNotRecognizedException;
+import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.XMLReader;
/**
@@ -31,14 +34,68 @@
* fixups) has to run on each {@link XMLReader} the factory produces. This wrapper returns a {@link HardeningSAXParser}, which applies that hardening lazily to
* both the SAX 2 {@link XMLReader} and the SAX 1 {@link org.xml.sax.Parser} it exposes.
*/
-final class HardeningSAXParserFactory extends DelegatingSAXParserFactory {
+final class HardeningSAXParserFactory extends SAXParserFactory {
+
+ private final SAXParserFactory delegate;
HardeningSAXParserFactory(final SAXParserFactory delegate) {
- super(delegate);
+ this.delegate = delegate;
}
@Override
public SAXParser newSAXParser() throws ParserConfigurationException, SAXException {
- return new HardeningSAXParser(super.newSAXParser());
+ return new HardeningSAXParser(delegate.newSAXParser());
+ }
+
+ //
+ @Override
+ public boolean getFeature(final String name) throws ParserConfigurationException, SAXNotRecognizedException, SAXNotSupportedException {
+ return delegate.getFeature(name);
+ }
+
+ @Override
+ public Schema getSchema() {
+ return delegate.getSchema();
+ }
+
+ @Override
+ public boolean isNamespaceAware() {
+ return delegate.isNamespaceAware();
+ }
+
+ @Override
+ public boolean isValidating() {
+ return delegate.isValidating();
+ }
+
+ @Override
+ public boolean isXIncludeAware() {
+ return delegate.isXIncludeAware();
+ }
+
+ @Override
+ public void setFeature(final String name, final boolean value) throws ParserConfigurationException, SAXNotRecognizedException, SAXNotSupportedException {
+ delegate.setFeature(name, value);
+ }
+
+ @Override
+ public void setNamespaceAware(final boolean awareness) {
+ delegate.setNamespaceAware(awareness);
+ }
+
+ @Override
+ public void setSchema(final Schema schema) {
+ delegate.setSchema(schema);
+ }
+
+ @Override
+ public void setValidating(final boolean validating) {
+ delegate.setValidating(validating);
+ }
+
+ @Override
+ public void setXIncludeAware(final boolean state) {
+ delegate.setXIncludeAware(state);
}
+ //
}
diff --git a/src/main/java/org/apache/commons/xml/HardeningSchema.java b/src/main/java/org/apache/commons/xml/HardeningSchema.java
index 5f0ad15..3b19c1e 100644
--- a/src/main/java/org/apache/commons/xml/HardeningSchema.java
+++ b/src/main/java/org/apache/commons/xml/HardeningSchema.java
@@ -23,9 +23,9 @@
/**
* {@link Schema} wrapper that hardens every {@link Validator} and {@link ValidatorHandler} the inner Schema produces: each {@link Validator} is wrapped in
- * {@link HardeningValidator} (which rewrites the Source through {@link XmlFactories#harden(javax.xml.transform.Source)} and installs the deny-all resolver), and
- * each {@link ValidatorHandler} gets the same deny-all {@link Resolvers.DenyAll#LS_RESOURCE} so {@code xsi:schemaLocation} is not resolved during SAX-driven
- * validation.
+ * {@link HardeningValidator} (which rewrites the Source through {@link XmlFactories#harden(javax.xml.transform.Source)} and installs the resolver floor), and
+ * each {@link ValidatorHandler} is wrapped in a {@link HardeningValidatorHandler} that keeps the same deny-all resolver floor so {@code xsi:schemaLocation} is
+ * not resolved during SAX-driven validation.
*/
final class HardeningSchema extends Schema {
@@ -42,8 +42,6 @@ public Validator newValidator() {
@Override
public ValidatorHandler newValidatorHandler() {
- final ValidatorHandler handler = delegate.newValidatorHandler();
- handler.setResourceResolver(Resolvers.DenyAll.LS_RESOURCE);
- return handler;
+ return new HardeningValidatorHandler(delegate.newValidatorHandler());
}
}
diff --git a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java
index e28f65e..537f40e 100644
--- a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java
@@ -23,7 +23,11 @@
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
+import org.w3c.dom.ls.LSResourceResolver;
+import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
+import org.xml.sax.SAXNotRecognizedException;
+import org.xml.sax.SAXNotSupportedException;
/**
* Capability-driven hardening wrapper for any {@link SchemaFactory} on the classpath, the same recipe for every implementation. It is the entry point reached
@@ -32,36 +36,52 @@
*
* Three layers cooperate:
*
- * - {@link HardeningSchemaFactory} installs a deny-all {@link Resolvers.DenyAll#LS_RESOURCE} on the factory (blocking
+ *
- {@link HardeningSchemaFactory} installs a deny-all {@link Resolvers.FallbackDenyLSResourceResolver} floor on the factory (blocking
* {@code xs:import}/{@code xs:include}/{@code xs:redefine} at compile time) and rewrites the Source on every {@code newSchema(Source[])} entry point
* through {@link XmlFactories#harden(Source)}.
- * - {@link HardeningSchema} wraps every Validator/ValidatorHandler the inner Schema produces and re-installs the deny-all resolver on each (blocking
+ *
- {@link HardeningSchema} wraps every Validator/ValidatorHandler the inner Schema produces and re-installs the floor on each (blocking
* {@code xsi:schemaLocation} at validation time), since neither the JDK nor Xerces reliably propagates it through {@code Schema}.
* - {@link HardeningValidator} rewrites the Source on every {@link Validator#validate(Source)} call.
*
*
* The hardened reader supplied by {@link XmlFactories#harden(Source)} already carries {@code FEATURE_SECURE_PROCESSING} and the processing limits, so a
* DOCTYPE, external entity or Billion Laughs payload in the schema or instance document is bounded there rather than on this factory. The JAXP 1.5
- * {@code ACCESS_EXTERNAL_*} properties are deliberately not set: the deny-all resolver already blocks the same fetches on every implementation, and the JDK 8
- * {@code SchemaFactory} has a bug whereby those properties keep blocking even when a caller's own resolver would grant the access, so leaving them unset lets a
- * caller re-enable specific lookups by swapping the resolver.
+ * {@code ACCESS_EXTERNAL_*} properties are deliberately not set: the resolver floor already blocks the same fetches on every implementation, and the JDK 8
+ * {@code SchemaFactory} has a bug whereby those properties keep blocking even when a caller's own resolver would grant the access. The floor is a non-removable
+ * lower bound: a caller-set {@link LSResourceResolver} is routed through it (opting a specific lookup in by returning a non-{@code null} result) rather than
+ * replacing it, so hardening cannot be dropped by swapping the resolver.
*/
-final class HardeningSchemaFactory extends DelegatingSchemaFactory {
+final class HardeningSchemaFactory extends SchemaFactory {
+
+ private final SchemaFactory delegate;
+
+ private final Resolvers.FallbackDenyLSResourceResolver floor = new Resolvers.FallbackDenyLSResourceResolver(null);
HardeningSchemaFactory(final SchemaFactory delegate) {
- super(delegate);
+ this.delegate = delegate;
// Compile-time block for xs:import/include/redefine; the wrappers carry the rest (per-product resolver, source rewriting, limits via the reader).
- delegate.setResourceResolver(Resolvers.DenyAll.LS_RESOURCE);
+ delegate.setResourceResolver(floor);
+ }
+
+ @Override
+ public void setResourceResolver(final LSResourceResolver resourceResolver) {
+ // Route a caller resolver through the floor instead of replacing it, so the deny-all lower bound cannot be removed.
+ floor.setDelegate(resourceResolver);
+ }
+
+ @Override
+ public LSResourceResolver getResourceResolver() {
+ return floor.getDelegate();
}
@Override
public Schema newSchema() throws SAXException {
- return new HardeningSchema(super.newSchema());
+ return new HardeningSchema(delegate.newSchema());
}
@Override
public Schema newSchema(final Source[] schemas) throws SAXException {
- return new HardeningSchema(super.newSchema(harden(schemas)));
+ return new HardeningSchema(delegate.newSchema(harden(schemas)));
}
private static Source[] harden(final Source[] schemas) throws SAXException {
@@ -75,4 +95,41 @@ private static Source[] harden(final Source[] schemas) throws SAXException {
}
return hardened;
}
+
+ //
+ @Override
+ public ErrorHandler getErrorHandler() {
+ return delegate.getErrorHandler();
+ }
+
+ @Override
+ public boolean getFeature(final String name) throws SAXNotRecognizedException, SAXNotSupportedException {
+ return delegate.getFeature(name);
+ }
+
+ @Override
+ public Object getProperty(final String name) throws SAXNotRecognizedException, SAXNotSupportedException {
+ return delegate.getProperty(name);
+ }
+
+ @Override
+ public boolean isSchemaLanguageSupported(final String schemaLanguage) {
+ return delegate.isSchemaLanguageSupported(schemaLanguage);
+ }
+
+ @Override
+ public void setErrorHandler(final ErrorHandler errorHandler) {
+ delegate.setErrorHandler(errorHandler);
+ }
+
+ @Override
+ public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException {
+ delegate.setFeature(name, value);
+ }
+
+ @Override
+ public void setProperty(final String name, final Object object) throws SAXNotRecognizedException, SAXNotSupportedException {
+ delegate.setProperty(name, object);
+ }
+ //
}
diff --git a/src/main/java/org/apache/commons/xml/HardeningTemplates.java b/src/main/java/org/apache/commons/xml/HardeningTemplates.java
index f7b947d..f53c17c 100644
--- a/src/main/java/org/apache/commons/xml/HardeningTemplates.java
+++ b/src/main/java/org/apache/commons/xml/HardeningTemplates.java
@@ -17,6 +17,8 @@
package org.apache.commons.xml;
+import java.util.Properties;
+
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
@@ -31,23 +33,31 @@
* and restoring it onto the runtime Transformer matches the JAXP-conformant intuition that the factory's resolver is the default for any Transformer the
* factory ultimately produces.
*/
-final class HardeningTemplates extends DelegatingTemplates {
+final class HardeningTemplates implements Templates {
+
+ private final Templates delegate;
/** Compile-time URIResolver snapshot; the underlying impl does not propagate the factory's resolver onto Transformers obtained from Templates. */
private final URIResolver uriResolver;
HardeningTemplates(final Templates delegate, final URIResolver uriResolver) {
- super(delegate);
+ this.delegate = delegate;
this.uriResolver = uriResolver;
}
@Override
public Transformer newTransformer() throws TransformerConfigurationException {
- final Transformer transformer = super.newTransformer();
+ final Transformer transformer = delegate.newTransformer();
if (transformer == null) {
return null;
}
- transformer.setURIResolver(uriResolver);
- return new HardeningTransformer(transformer);
+ return new HardeningTransformer(transformer, uriResolver);
+ }
+
+ //
+ @Override
+ public Properties getOutputProperties() {
+ return delegate.getOutputProperties();
}
+ //
}
diff --git a/src/main/java/org/apache/commons/xml/HardeningTransformer.java b/src/main/java/org/apache/commons/xml/HardeningTransformer.java
index 7e77f2e..577051b 100644
--- a/src/main/java/org/apache/commons/xml/HardeningTransformer.java
+++ b/src/main/java/org/apache/commons/xml/HardeningTransformer.java
@@ -17,28 +17,104 @@
package org.apache.commons.xml;
+import java.util.Properties;
+
+import javax.xml.transform.ErrorListener;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
+import javax.xml.transform.URIResolver;
/**
* {@link Transformer} wrapper that rewrites the Source on every {@link Transformer#transform(Source, Result)} call through
- * {@link XmlFactories#harden(Source)} before delegating.
+ * {@link XmlFactories#harden(Source)} before delegating, and keeps a deny-all {@link URIResolver} floor so runtime {@code document()} calls a caller does not
+ * resolve are denied rather than fetched.
+ *
+ * The floor is installed on the delegate transformer at construction, seeded with the factory's compile-time resolver; {@link #setURIResolver(URIResolver)}
+ * routes a caller's resolver through it rather than replacing it, so the block cannot be dropped.
*/
-final class HardeningTransformer extends DelegatingTransformer {
+final class HardeningTransformer extends Transformer {
+
+ private final Transformer delegate;
+
+ private final Resolvers.FallbackDenyURIResolver floor;
+
+ HardeningTransformer(final Transformer delegate, final URIResolver uriResolver) {
+ this.delegate = delegate;
+ this.floor = new Resolvers.FallbackDenyURIResolver(uriResolver);
+ delegate.setURIResolver(floor);
+ }
- HardeningTransformer(final Transformer delegate) {
- super(delegate);
+ @Override
+ public void setURIResolver(final URIResolver resolver) {
+ floor.setDelegate(resolver);
+ }
+
+ @Override
+ public URIResolver getURIResolver() {
+ return floor.getDelegate();
}
@Override
public void transform(final Source xmlSource, final Result outputTarget) throws TransformerException {
try {
- super.transform(XmlFactories.harden(xmlSource), outputTarget);
+ delegate.transform(XmlFactories.harden(xmlSource), outputTarget);
} catch (final TransformerConfigurationException e) {
throw new TransformerException(e);
}
}
+
+ //
+ @Override
+ public void clearParameters() {
+ delegate.clearParameters();
+ }
+
+ @Override
+ public ErrorListener getErrorListener() {
+ return delegate.getErrorListener();
+ }
+
+ @Override
+ public Properties getOutputProperties() {
+ return delegate.getOutputProperties();
+ }
+
+ @Override
+ public String getOutputProperty(final String name) {
+ return delegate.getOutputProperty(name);
+ }
+
+ @Override
+ public Object getParameter(final String name) {
+ return delegate.getParameter(name);
+ }
+
+ @Override
+ public void reset() {
+ delegate.reset();
+ }
+
+ @Override
+ public void setErrorListener(final ErrorListener listener) {
+ delegate.setErrorListener(listener);
+ }
+
+ @Override
+ public void setOutputProperties(final Properties properties) {
+ delegate.setOutputProperties(properties);
+ }
+
+ @Override
+ public void setOutputProperty(final String name, final String value) {
+ delegate.setOutputProperty(name, value);
+ }
+
+ @Override
+ public void setParameter(final String name, final Object value) {
+ delegate.setParameter(name, value);
+ }
+ //
}
diff --git a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java
index 2e54465..8d61bc6 100644
--- a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java
@@ -17,14 +17,18 @@
package org.apache.commons.xml;
+import javax.xml.transform.ErrorListener;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
+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 org.xml.sax.XMLFilter;
import org.xml.sax.XMLReader;
/**
@@ -52,39 +56,112 @@
* runtime source rewrite.
*
*/
-final class HardeningTransformerFactory extends DelegatingTransformerFactory {
+final class HardeningTransformerFactory extends SAXTransformerFactory {
+
+ private final SAXTransformerFactory delegate;
+
+ private final Resolvers.FallbackDenyURIResolver floor = new Resolvers.FallbackDenyURIResolver(null);
HardeningTransformerFactory(final SAXTransformerFactory delegate) {
- super(delegate);
+ this.delegate = delegate;
+ // Compile-time block for xsl:import/xsl:include and document(); a caller-set resolver is routed through the floor rather than replacing it.
+ delegate.setURIResolver(floor);
+ }
+
+ @Override
+ public void setURIResolver(final URIResolver resolver) {
+ floor.setDelegate(resolver);
+ }
+
+ @Override
+ public URIResolver getURIResolver() {
+ return floor.getDelegate();
}
@Override
public Source getAssociatedStylesheet(final Source source, final String media, final String title, final String charset)
throws TransformerConfigurationException {
- return super.getAssociatedStylesheet(XmlFactories.harden(source), media, title, charset);
+ return delegate.getAssociatedStylesheet(XmlFactories.harden(source), media, title, charset);
}
@Override
public Templates newTemplates(final Source source) throws TransformerConfigurationException {
- final Templates templates = super.newTemplates(XmlFactories.harden(source));
+ final Templates templates = delegate.newTemplates(XmlFactories.harden(source));
return templates == null ? null : new HardeningTemplates(templates, getURIResolver());
}
@Override
public Transformer newTransformer() throws TransformerConfigurationException {
// Identity transformer: still parses runtime sources, so wrap it to harden Transformer.transform(Source, Result).
- final Transformer transformer = super.newTransformer();
- return transformer == null ? null : new HardeningTransformer(transformer);
+ final Transformer transformer = delegate.newTransformer();
+ return transformer == null ? null : new HardeningTransformer(transformer, getURIResolver());
}
@Override
public Transformer newTransformer(final Source source) throws TransformerConfigurationException {
- final Transformer transformer = super.newTransformer(XmlFactories.harden(source));
- return transformer == null ? null : new HardeningTransformer(transformer);
+ final Transformer transformer = delegate.newTransformer(XmlFactories.harden(source));
+ return transformer == null ? null : new HardeningTransformer(transformer, getURIResolver());
}
@Override
public TransformerHandler newTransformerHandler(final Source source) throws TransformerConfigurationException {
- return super.newTransformerHandler(XmlFactories.harden(source));
+ return delegate.newTransformerHandler(XmlFactories.harden(source));
+ }
+
+ //
+ @Override
+ public Object getAttribute(final String name) {
+ return delegate.getAttribute(name);
+ }
+
+ @Override
+ public ErrorListener getErrorListener() {
+ return delegate.getErrorListener();
+ }
+
+ @Override
+ public boolean getFeature(final String name) {
+ return delegate.getFeature(name);
+ }
+
+ @Override
+ public TemplatesHandler newTemplatesHandler() throws TransformerConfigurationException {
+ return delegate.newTemplatesHandler();
+ }
+
+ @Override
+ public TransformerHandler newTransformerHandler() throws TransformerConfigurationException {
+ return delegate.newTransformerHandler();
+ }
+
+ @Override
+ public TransformerHandler newTransformerHandler(final Templates templates) throws TransformerConfigurationException {
+ return delegate.newTransformerHandler(templates);
+ }
+
+ @Override
+ public XMLFilter newXMLFilter(final Source source) throws TransformerConfigurationException {
+ return delegate.newXMLFilter(source);
+ }
+
+ @Override
+ public XMLFilter newXMLFilter(final Templates templates) throws TransformerConfigurationException {
+ return delegate.newXMLFilter(templates);
+ }
+
+ @Override
+ public void setAttribute(final String name, final Object value) {
+ delegate.setAttribute(name, value);
+ }
+
+ @Override
+ public void setErrorListener(final ErrorListener listener) {
+ delegate.setErrorListener(listener);
+ }
+
+ @Override
+ public void setFeature(final String name, final boolean value) throws TransformerConfigurationException {
+ delegate.setFeature(name, value);
}
+ //
}
diff --git a/src/main/java/org/apache/commons/xml/HardeningValidator.java b/src/main/java/org/apache/commons/xml/HardeningValidator.java
index 86dc063..8eaa4b2 100644
--- a/src/main/java/org/apache/commons/xml/HardeningValidator.java
+++ b/src/main/java/org/apache/commons/xml/HardeningValidator.java
@@ -32,18 +32,20 @@
/**
* {@link Validator} wrapper that rewrites the Source on every {@link Validator#validate(Source)} and {@link Validator#validate(Source, Result)} call through
- * {@link XmlFactories#harden(Source)} before delegating, and installs a deny-all {@link LSResourceResolver} so {@code xsi:schemaLocation} is not resolved at
+ * {@link XmlFactories#harden(Source)} before delegating, and keeps a deny-all {@link LSResourceResolver} floor so {@code xsi:schemaLocation} is not resolved at
* validation time.
*/
final class HardeningValidator extends Validator {
private final Validator delegate;
+ private final Resolvers.FallbackDenyLSResourceResolver floor = new Resolvers.FallbackDenyLSResourceResolver(null);
+
HardeningValidator(final Validator delegate) {
this.delegate = delegate;
- // Block xsi:schemaLocation resolution; neither the JDK nor Xerces reliably propagates the factory's resolver to its Validators. A caller may re-enable
- // specific lookups by setting their own resolver afterwards.
- delegate.setResourceResolver(Resolvers.DenyAll.LS_RESOURCE);
+ // Block xsi:schemaLocation resolution; neither the JDK nor Xerces reliably propagates the factory's resolver to its Validators. The floor is a
+ // non-removable lower bound: a caller opts specific lookups in by setting their own resolver, but cannot drop the deny-all block.
+ delegate.setResourceResolver(floor);
}
@Override
@@ -63,7 +65,7 @@ public Object getProperty(final String name) throws SAXNotRecognizedException, S
@Override
public LSResourceResolver getResourceResolver() {
- return delegate.getResourceResolver();
+ return floor.getDelegate();
}
@Override
@@ -88,7 +90,8 @@ public void setProperty(final String name, final Object object) throws SAXNotRec
@Override
public void setResourceResolver(final LSResourceResolver resourceResolver) {
- delegate.setResourceResolver(resourceResolver);
+ // Route a caller resolver through the floor instead of replacing it, so the deny-all lower bound cannot be removed.
+ floor.setDelegate(resourceResolver);
}
@Override
diff --git a/src/main/java/org/apache/commons/xml/HardeningValidatorHandler.java b/src/main/java/org/apache/commons/xml/HardeningValidatorHandler.java
new file mode 100644
index 0000000..aa56dd8
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/HardeningValidatorHandler.java
@@ -0,0 +1,160 @@
+/*
+ * 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
+ *
+ * https://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.commons.xml;
+
+import javax.xml.validation.TypeInfoProvider;
+import javax.xml.validation.ValidatorHandler;
+
+import org.w3c.dom.ls.LSResourceResolver;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXNotRecognizedException;
+import org.xml.sax.SAXNotSupportedException;
+
+/**
+ * {@link ValidatorHandler} wrapper that keeps a deny-all {@link LSResourceResolver} floor a caller cannot remove.
+ *
+ * Blocks {@code xsi:schemaLocation} resolution during SAX-driven validation. A caller-set resolver is routed through a {@link
+ * Resolvers.FallbackDenyLSResourceResolver} rather than replacing the floor, so a schema the caller does not resolve is denied instead of fetched.
+ */
+final class HardeningValidatorHandler extends ValidatorHandler {
+
+ private final ValidatorHandler delegate;
+
+ private final Resolvers.FallbackDenyLSResourceResolver floor = new Resolvers.FallbackDenyLSResourceResolver(null);
+
+ HardeningValidatorHandler(final ValidatorHandler delegate) {
+ this.delegate = delegate;
+ delegate.setResourceResolver(floor);
+ }
+
+ @Override
+ public void setResourceResolver(final LSResourceResolver resourceResolver) {
+ floor.setDelegate(resourceResolver);
+ }
+
+ @Override
+ public LSResourceResolver getResourceResolver() {
+ return floor.getDelegate();
+ }
+
+ //
+ @Override
+ public void setContentHandler(final ContentHandler receiver) {
+ delegate.setContentHandler(receiver);
+ }
+
+ @Override
+ public ContentHandler getContentHandler() {
+ return delegate.getContentHandler();
+ }
+
+ @Override
+ public void setErrorHandler(final ErrorHandler errorHandler) {
+ delegate.setErrorHandler(errorHandler);
+ }
+
+ @Override
+ public ErrorHandler getErrorHandler() {
+ return delegate.getErrorHandler();
+ }
+
+ @Override
+ public TypeInfoProvider getTypeInfoProvider() {
+ return delegate.getTypeInfoProvider();
+ }
+
+ @Override
+ public boolean getFeature(final String name) throws SAXNotRecognizedException, SAXNotSupportedException {
+ return delegate.getFeature(name);
+ }
+
+ @Override
+ public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException {
+ delegate.setFeature(name, value);
+ }
+
+ @Override
+ public Object getProperty(final String name) throws SAXNotRecognizedException, SAXNotSupportedException {
+ return delegate.getProperty(name);
+ }
+
+ @Override
+ public void setProperty(final String name, final Object object) throws SAXNotRecognizedException, SAXNotSupportedException {
+ delegate.setProperty(name, object);
+ }
+
+ @Override
+ public void setDocumentLocator(final 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(final String prefix, final String uri) throws SAXException {
+ delegate.startPrefixMapping(prefix, uri);
+ }
+
+ @Override
+ public void endPrefixMapping(final String prefix) throws SAXException {
+ delegate.endPrefixMapping(prefix);
+ }
+
+ @Override
+ public void startElement(final String uri, final String localName, final String qName, final Attributes atts) throws SAXException {
+ delegate.startElement(uri, localName, qName, atts);
+ }
+
+ @Override
+ public void endElement(final String uri, final String localName, final String qName) throws SAXException {
+ delegate.endElement(uri, localName, qName);
+ }
+
+ @Override
+ public void characters(final char[] ch, final int start, final int length) throws SAXException {
+ delegate.characters(ch, start, length);
+ }
+
+ @Override
+ public void ignorableWhitespace(final char[] ch, final int start, final int length) throws SAXException {
+ delegate.ignorableWhitespace(ch, start, length);
+ }
+
+ @Override
+ public void processingInstruction(final String target, final String data) throws SAXException {
+ delegate.processingInstruction(target, data);
+ }
+
+ @Override
+ public void skippedEntity(final String name) throws SAXException {
+ delegate.skippedEntity(name);
+ }
+ //
+}
diff --git a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java
new file mode 100644
index 0000000..4b41d48
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java
@@ -0,0 +1,214 @@
+/*
+ * 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
+ *
+ * https://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.commons.xml;
+
+import java.io.InputStream;
+import java.io.Reader;
+
+import javax.xml.stream.EventFilter;
+import javax.xml.stream.StreamFilter;
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLReporter;
+import javax.xml.stream.XMLResolver;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.util.XMLEventAllocator;
+import javax.xml.transform.Source;
+
+/**
+ * {@link XMLInputFactory} wrapper that keeps the {@link Resolvers.FallbackDenyXMLResolver} floors {@link StaxHardener} installs on the entity-resolution hooks
+ * non-removable by the caller.
+ *
+ * Every resolver-valued entry point ({@link #setXMLResolver(XMLResolver)}, {@code setProperty(XMLInputFactory.RESOLVER, ...)} and the Woodstox
+ * {@code com.ctc.wstx.*Resolver} keys) is routed uniformly: a caller who supplies their own {@link Resolvers.FallbackDenyXMLResolver} takes control and it is
+ * passed straight to the delegate; otherwise the current resolver on that hook is read, and if it is one of our floors the caller's resolver is set as its
+ * {@link Resolvers.FallbackDenyXMLResolver#setDelegate delegate} (an opt-in the floor cannot be removed by), or, if the hook is empty, the caller's resolver is
+ * wrapped in a fresh floor. This matters because Woodstox does not chain resolvers: when a resolver returns {@code null}, {@code DefaultInputResolver} falls
+ * through to fetching the systemId URL itself, so a caller-set resolver that returns {@code null} must still land behind the floor. {@link #getXMLResolver()} and
+ * {@code getProperty} report the caller's resolver unwrapped.
+ */
+final class HardeningXMLInputFactory extends XMLInputFactory {
+
+ private final XMLInputFactory delegate;
+
+ HardeningXMLInputFactory(final XMLInputFactory delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void setXMLResolver(final XMLResolver resolver) {
+ setResolverProperty(XMLInputFactory.RESOLVER, resolver);
+ }
+
+ @Override
+ public XMLResolver getXMLResolver() {
+ return unwrap(delegate.getXMLResolver());
+ }
+
+ @Override
+ public void setProperty(final String name, final Object value) {
+ // If a resolver property has a value of the wrong type, pass it to the delegate to generate an appropriate exception.
+ if (isResolverProperty(name) && (value == null || value instanceof XMLResolver)) {
+ setResolverProperty(name, (XMLResolver) value);
+ } else {
+ delegate.setProperty(name, value);
+ }
+ }
+
+ @Override
+ public Object getProperty(final String name) {
+ if (isResolverProperty(name)) {
+ return unwrap((XMLResolver) delegate.getProperty(name));
+ }
+ return delegate.getProperty(name);
+ }
+
+ /**
+ * Routes a caller-set resolver for the property {@code name} behind the floor currently installed on that hook.
+ *
+ * @param name the resolver-valued property being set.
+ * @param resolver the caller's resolver, or their own {@link Resolvers.FallbackDenyXMLResolver} to take control.
+ */
+ private void setResolverProperty(final String name, final XMLResolver resolver) {
+ if (resolver instanceof Resolvers.FallbackDenyXMLResolver) {
+ // The caller supplies their own floor: hand it to the delegate as-is.
+ delegate.setProperty(name, resolver);
+ } else {
+ final Object current = delegate.getProperty(name);
+ if (current instanceof Resolvers.FallbackDenyXMLResolver) {
+ ((Resolvers.FallbackDenyXMLResolver) current).setDelegate(resolver);
+ } else {
+ delegate.setProperty(name, new Resolvers.FallbackDenyXMLResolver(resolver));
+ }
+ }
+ }
+
+ private static boolean isResolverProperty(final String name) {
+ return XMLInputFactory.RESOLVER.equals(name)
+ || StaxHardener.WSTX_DTD_RESOLVER.equals(name)
+ || StaxHardener.WSTX_ENTITY_RESOLVER.equals(name)
+ || StaxHardener.WSTX_UNDECLARED_ENTITY_RESOLVER.equals(name);
+ }
+
+ private static XMLResolver unwrap(final XMLResolver resolver) {
+ return resolver instanceof Resolvers.FallbackDenyXMLResolver ? ((Resolvers.FallbackDenyXMLResolver) resolver).getDelegate() : resolver;
+ }
+
+ //
+ @Override
+ public XMLStreamReader createXMLStreamReader(final Reader reader) throws XMLStreamException {
+ return delegate.createXMLStreamReader(reader);
+ }
+
+ @Override
+ public XMLStreamReader createXMLStreamReader(final Source source) throws XMLStreamException {
+ return delegate.createXMLStreamReader(source);
+ }
+
+ @Override
+ public XMLStreamReader createXMLStreamReader(final InputStream stream) throws XMLStreamException {
+ return delegate.createXMLStreamReader(stream);
+ }
+
+ @Override
+ public XMLStreamReader createXMLStreamReader(final InputStream stream, final String encoding) throws XMLStreamException {
+ return delegate.createXMLStreamReader(stream, encoding);
+ }
+
+ @Override
+ public XMLStreamReader createXMLStreamReader(final String systemId, final InputStream stream) throws XMLStreamException {
+ return delegate.createXMLStreamReader(systemId, stream);
+ }
+
+ @Override
+ public XMLStreamReader createXMLStreamReader(final String systemId, final Reader reader) throws XMLStreamException {
+ return delegate.createXMLStreamReader(systemId, reader);
+ }
+
+ @Override
+ public XMLEventReader createXMLEventReader(final Reader reader) throws XMLStreamException {
+ return delegate.createXMLEventReader(reader);
+ }
+
+ @Override
+ public XMLEventReader createXMLEventReader(final String systemId, final Reader reader) throws XMLStreamException {
+ return delegate.createXMLEventReader(systemId, reader);
+ }
+
+ @Override
+ public XMLEventReader createXMLEventReader(final XMLStreamReader reader) throws XMLStreamException {
+ return delegate.createXMLEventReader(reader);
+ }
+
+ @Override
+ public XMLEventReader createXMLEventReader(final Source source) throws XMLStreamException {
+ return delegate.createXMLEventReader(source);
+ }
+
+ @Override
+ public XMLEventReader createXMLEventReader(final InputStream stream) throws XMLStreamException {
+ return delegate.createXMLEventReader(stream);
+ }
+
+ @Override
+ public XMLEventReader createXMLEventReader(final InputStream stream, final String encoding) throws XMLStreamException {
+ return delegate.createXMLEventReader(stream, encoding);
+ }
+
+ @Override
+ public XMLEventReader createXMLEventReader(final String systemId, final InputStream stream) throws XMLStreamException {
+ return delegate.createXMLEventReader(systemId, stream);
+ }
+
+ @Override
+ public XMLStreamReader createFilteredReader(final XMLStreamReader reader, final StreamFilter filter) throws XMLStreamException {
+ return delegate.createFilteredReader(reader, filter);
+ }
+
+ @Override
+ public XMLEventReader createFilteredReader(final XMLEventReader reader, final EventFilter filter) throws XMLStreamException {
+ return delegate.createFilteredReader(reader, filter);
+ }
+
+ @Override
+ public XMLReporter getXMLReporter() {
+ return delegate.getXMLReporter();
+ }
+
+ @Override
+ public void setXMLReporter(final XMLReporter reporter) {
+ delegate.setXMLReporter(reporter);
+ }
+
+ @Override
+ public boolean isPropertySupported(final String name) {
+ return delegate.isPropertySupported(name);
+ }
+
+ @Override
+ public void setEventAllocator(final XMLEventAllocator allocator) {
+ delegate.setEventAllocator(allocator);
+ }
+
+ @Override
+ public XMLEventAllocator getEventAllocator() {
+ return delegate.getEventAllocator();
+ }
+ //
+}
diff --git a/src/main/java/org/apache/commons/xml/DelegatingXMLReader.java b/src/main/java/org/apache/commons/xml/HardeningXMLReader.java
similarity index 63%
rename from src/main/java/org/apache/commons/xml/DelegatingXMLReader.java
rename to src/main/java/org/apache/commons/xml/HardeningXMLReader.java
index 9b50614..763f05a 100644
--- a/src/main/java/org/apache/commons/xml/DelegatingXMLReader.java
+++ b/src/main/java/org/apache/commons/xml/HardeningXMLReader.java
@@ -30,16 +30,44 @@
import org.xml.sax.XMLReader;
/**
- * {@link XMLReader} that forwards every method to a wrapped delegate.
+ * {@link XMLReader} wrapper that keeps a {@link Resolvers.FallbackDenyResolver} floor as the reader's entity resolver, non-overridable by the caller.
+ *
+ * The floor is installed once and stays the reader's entity resolver for the wrapper's lifetime; {@link #setEntityResolver(EntityResolver)} routes the
+ * caller's resolver through {@link Resolvers.FallbackDenyResolver#setDelegate} instead of replacing it. This includes the {@code DefaultHandler} that
+ * {@link javax.xml.parsers.SAXParser#parse(org.xml.sax.InputSource, org.xml.sax.helpers.DefaultHandler) SAXParser.parse(source, handler)} installs as the
+ * reader's entity resolver, which would otherwise silently replace the floor. {@link #getEntityResolver()} reports the caller's resolver unwrapped.
+ *
+ * A path that needs a non-deny floor (e.g. one that also permits the external DTD subset) passes a {@link Resolvers.FallbackDenyResolver} subclass to the
+ * two-argument constructor; a single stable floor instance also lets that subclass double as a {@link org.xml.sax.ext.LexicalHandler}. Every other method
+ * forwards to the wrapped delegate; subclasses (e.g. {@code HardeningExpatXMLReader}) add per-implementation fixups on top of the floor.
*/
-class DelegatingXMLReader implements XMLReader {
+class HardeningXMLReader implements XMLReader {
private final XMLReader delegate;
- DelegatingXMLReader(final XMLReader delegate) {
+ private final Resolvers.FallbackDenyResolver floor;
+
+ HardeningXMLReader(final XMLReader delegate) {
+ this(delegate, new Resolvers.FallbackDenyResolver(null));
+ }
+
+ HardeningXMLReader(final XMLReader delegate, final Resolvers.FallbackDenyResolver floor) {
this.delegate = delegate;
+ this.floor = floor;
+ delegate.setEntityResolver(floor);
}
+ @Override
+ public void setEntityResolver(final EntityResolver resolver) {
+ floor.setDelegate(resolver);
+ }
+
+ @Override
+ public EntityResolver getEntityResolver() {
+ return floor.getDelegate();
+ }
+
+ //
@Override
public ContentHandler getContentHandler() {
return delegate.getContentHandler();
@@ -50,11 +78,6 @@ public DTDHandler getDTDHandler() {
return delegate.getDTDHandler();
}
- @Override
- public EntityResolver getEntityResolver() {
- return delegate.getEntityResolver();
- }
-
@Override
public ErrorHandler getErrorHandler() {
return delegate.getErrorHandler();
@@ -90,11 +113,6 @@ public void setDTDHandler(final DTDHandler handler) {
delegate.setDTDHandler(handler);
}
- @Override
- public void setEntityResolver(final EntityResolver resolver) {
- delegate.setEntityResolver(resolver);
- }
-
@Override
public void setErrorHandler(final ErrorHandler handler) {
delegate.setErrorHandler(handler);
@@ -109,4 +127,5 @@ public void setFeature(final String name, final boolean value) throws SAXNotReco
public void setProperty(final String name, final Object value) throws SAXNotRecognizedException, SAXNotSupportedException {
delegate.setProperty(name, value);
}
+ //
}
diff --git a/src/main/java/org/apache/commons/xml/Resolvers.java b/src/main/java/org/apache/commons/xml/Resolvers.java
index 63556c4..17d89ef 100644
--- a/src/main/java/org/apache/commons/xml/Resolvers.java
+++ b/src/main/java/org/apache/commons/xml/Resolvers.java
@@ -18,117 +18,288 @@
package org.apache.commons.xml;
import java.io.ByteArrayInputStream;
+import java.io.IOException;
import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
import javax.xml.stream.XMLResolver;
import javax.xml.stream.XMLStreamException;
+import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.URIResolver;
+import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSResourceResolver;
+import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
+import org.xml.sax.ext.DefaultHandler2;
import org.xml.sax.ext.EntityResolver2;
/**
- * Stateless resolver singletons that fix the outcome of every external lookup.
+ * Policy resolvers that fix the outcome of every external lookup. Each member is a floor with two defining properties:
*
- * Two flavours are exposed, each as a typed singleton field per resolver interface:
- *
- * - {@link DenyAll} refuses every lookup with an exception. Use this on schema/XSLT compile paths and on parser entity hooks where any external fetch is
- * a hardening violation.
- * - {@link IgnoreAll} returns an empty input. Use this on Woodstox's DTD-subset and undeclared-entity hooks where the parse must continue with no
- * replacement content.
- *
+ *
+ * - Non-removable, and it wraps the resolver the caller sets. The hardened wrappers install one and route a caller-set resolver through
+ * {@code setDelegate} rather than letting it replace the floor, so the caller's resolver is consulted first but cannot remove the floor underneath it.
+ * - It supplies the default action for a lookup the caller's resolver does not resolve (a {@code null} return, or no caller resolver at
+ * all). This is where a floor departs from stock JAXP: normally an unresolved lookup falls back to the processor's built-in resolution and the resource is
+ * fetched; a floor instead denies it (throws).
+ *
*
- * {@link XMLResolver} and {@link EntityResolver2} both declare a 4-arg {@code resolveEntity(String, String, String, String)} with identical erasure but
- * different parameter semantics, return types ({@link Object} vs {@link InputSource}) and throws clauses ({@link XMLStreamException} vs {@link SAXException}),
- * so they cannot coexist on the same class. Each flavour therefore exposes its {@code XMLResolver} and {@code EntityResolver2} singletons separately.
+ * The deny members are {@link FallbackDenyResolver} (for {@code EntityResolver}), {@link FallbackDenyLSResourceResolver} (for {@link LSResourceResolver}),
+ * {@link FallbackDenyURIResolver} (for {@link URIResolver}) and {@link FallbackDenyXMLResolver} (for {@link XMLResolver}). {@link FallbackIgnoreXMLResolver} is a
+ * variant whose default action returns an empty input instead of throwing, for the Woodstox DTD-subset and undeclared-entity hooks where a missing resource must
+ * be skipped rather than denied.
*/
final class Resolvers {
/**
- * Refuses every external resource lookup with an exception.
+ * Entity resolver that consults an optional caller-supplied resolver and denies (throws) whatever the caller does not resolve.
*
- * The single-method resolvers ({@link LSResourceResolver}, {@link URIResolver}, {@link XMLResolver}) are exposed as lambdas; {@link EntityResolver2}
- * declares three methods, so it lives in a private nested class.
+ * This is the entity-resolution counterpart of the JAXP 1.5 {@code ACCESS_EXTERNAL_*} properties: a non-overridable floor. The hardened DOM and SAX
+ * wrappers install one of these and, when the caller sets their own {@link EntityResolver}, route it through {@link #setDelegate} rather than letting it
+ * replace the floor. A caller therefore opts a specific resource in by returning a non-{@code null} {@link InputSource} from their resolver; anything they
+ * leave unresolved (a {@code null} return, or no caller resolver at all) goes to {@link #onUnresolved}, which denies by default.
+ *
+ * It extends {@link DefaultHandler2} so it is also usable as a {@link org.xml.sax.ext.LexicalHandler} (see {@code SAXParserHardener}'s Android subclass,
+ * which needs {@code startDTD}/{@code endDTD}); {@link #getExternalSubset} therefore inherits the {@code DefaultHandler2} "no synthetic subset" default. Only
+ * {@link #resolveEntity(String, String, String, String) resolveEntity} (the actual external fetch) reaches the deny fallback.
*/
- static final class DenyAll {
+ static class FallbackDenyResolver extends DefaultHandler2 {
/**
- * {@link EntityResolver2}: refuses every external entity lookup performed by a SAX or DOM parser.
+ * Caller-supplied resolver consulted first, or {@code null} for a pure deny-all floor.
*/
- private static final class DenyAllEntityResolver2 implements EntityResolver2 {
+ private EntityResolver delegate;
- private DenyAllEntityResolver2() {
- }
+ FallbackDenyResolver(final EntityResolver delegate) {
+ this.delegate = delegate;
+ }
- @Override
- public InputSource getExternalSubset(final String name, final String baseURI) {
- // Canonical EntityResolver2 "no synthetic subset" signal; matches the behavior of an absent resolver. Blocking happens in resolveEntity below.
- return null;
- }
+ /**
+ * Replaces the caller resolver consulted ahead of the floor; lets a single floor instance back successive {@code setEntityResolver} calls.
+ *
+ * @param delegate the caller-supplied resolver, or {@code null} for a pure deny-all floor.
+ */
+ final void setDelegate(final EntityResolver delegate) {
+ this.delegate = delegate;
+ }
- @Override
- public InputSource resolveEntity(final String publicId, final String systemId) throws SAXException {
- throw new SAXException(forbiddenMessage(null, null, publicId, systemId, null));
- }
+ final EntityResolver getDelegate() {
+ return delegate;
+ }
- @Override
- public InputSource resolveEntity(final String name, final String publicId, final String baseURI, final String systemId) throws SAXException {
- throw new SAXException(forbiddenMessage(name, null, publicId, systemId, baseURI));
- }
+ @Override
+ public final InputSource resolveEntity(final String publicId, final String systemId) throws SAXException, IOException {
+ return resolveEntity(null, publicId, null, systemId);
}
- /**
- * Refuses every external entity lookup performed by a SAX or DOM parser, including the external DTD subset.
- */
- static final EntityResolver2 ENTITY2 = new DenyAllEntityResolver2();
+ @Override
+ public final InputSource resolveEntity(final String name, final String publicId, final String baseURI, final String systemId)
+ throws SAXException, IOException {
+ final InputSource resolved = resolveWithDelegate(name, publicId, baseURI, systemId);
+ return resolved != null ? resolved : onUnresolved(name, publicId, baseURI, systemId);
+ }
/**
- * Refuses every {@code xs:import}/{@code xs:include}/{@code xs:redefine} lookup at schema-compile time.
+ * Outcome when neither the caller delegate nor this resolver provides the entity. Denies by default; a subclass may permit specific lookups (e.g. the
+ * external DTD subset) by returning {@code null} or an {@link InputSource} instead of calling {@code super}.
+ *
+ * @param name the entity name, or {@code null} on the 2-arg resolution path.
+ * @param publicId the public identifier, or {@code null} if none.
+ * @param baseURI the base URI for relative resolution, or {@code null}.
+ * @param systemId the system identifier of the unresolved entity.
+ * @return an {@link InputSource} to permit the lookup, or {@code null} to skip it silently; the default implementation never returns normally.
+ * @throws SAXException to deny the lookup (the default behavior).
+ * @throws IOException if a subclass opens a stream that fails.
*/
- static final LSResourceResolver LS_RESOURCE = (type, namespaceURI, publicId, systemId, baseURI) -> {
+ protected InputSource onUnresolved(final String name, final String publicId, final String baseURI, final String systemId)
+ throws SAXException, IOException {
+ throw new SAXException(forbiddenMessage(name, null, publicId, systemId, baseURI));
+ }
+
+ private InputSource resolveWithDelegate(final String name, final String publicId, final String baseURI,
+ final String systemId) throws SAXException, IOException {
+ if (delegate != null) {
+ return delegate instanceof EntityResolver2 ? ((EntityResolver2) delegate).resolveEntity(name, publicId, baseURI, systemId) :
+ // We need to resolve the systemId against baseURI, because a plain EntityResolver expects an absolute URI.
+ delegate.resolveEntity(publicId, absolutize(baseURI, systemId));
+ }
+ return null;
+ }
+ }
+
+ /**
+ * {@link LSResourceResolver} floor: consults an optional caller-supplied resolver and denies (throws) whatever the caller does not resolve.
+ *
+ * The schema-compile counterpart of {@link FallbackDenyResolver}. The hardened {@link javax.xml.validation.SchemaFactory}, {@link
+ * javax.xml.validation.Validator} and {@link javax.xml.validation.ValidatorHandler} wrappers install one of these and route a caller-set resolver through
+ * {@link #setDelegate} rather than letting it replace the floor. A caller opts a specific resource in by returning a non-{@code null} {@link LSInput};
+ * anything left unresolved is denied.
+ */
+ static final class FallbackDenyLSResourceResolver implements LSResourceResolver {
+
+ private LSResourceResolver delegate;
+
+ FallbackDenyLSResourceResolver(final LSResourceResolver delegate) {
+ this.delegate = delegate;
+ }
+
+ void setDelegate(final LSResourceResolver delegate) {
+ this.delegate = delegate;
+ }
+
+ LSResourceResolver getDelegate() {
+ return delegate;
+ }
+
+ @Override
+ public LSInput resolveResource(final String type, final String namespaceURI, final String publicId, final String systemId, final String baseURI) {
+ final LSInput resolved = delegate != null ? delegate.resolveResource(type, namespaceURI, publicId, systemId, baseURI) : null;
+ if (resolved != null) {
+ return resolved;
+ }
throw new SecurityException(forbiddenMessage(type, namespaceURI, publicId, systemId, baseURI));
- };
+ }
+ }
- /**
- * Refuses every {@code xsl:import}/{@code xsl:include}/{@code document()} lookup during XSLT compile and transform.
- */
- static final URIResolver URI = (href, base) -> {
+ /**
+ * {@link URIResolver} floor: consults an optional caller-supplied resolver and denies (throws) whatever the caller does not resolve.
+ *
+ * The XSLT counterpart of {@link FallbackDenyResolver}, guarding {@code xsl:import}/{@code xsl:include} at compile time and {@code document()} at
+ * transform time. The hardened {@link javax.xml.transform.TransformerFactory} and {@link javax.xml.transform.Transformer} wrappers install one of these and
+ * route a caller-set resolver through {@link #setDelegate} rather than letting it replace the floor. A caller opts a specific URI in by returning a
+ * non-{@code null} {@link Source}; anything left unresolved is denied.
+ */
+ static final class FallbackDenyURIResolver implements URIResolver {
+
+ private URIResolver delegate;
+
+ FallbackDenyURIResolver(final URIResolver delegate) {
+ this.delegate = delegate;
+ }
+
+ void setDelegate(final URIResolver delegate) {
+ this.delegate = delegate;
+ }
+
+ URIResolver getDelegate() {
+ return delegate;
+ }
+
+ @Override
+ public Source resolve(final String href, final String base) throws TransformerException {
+ final Source resolved = delegate != null ? delegate.resolve(href, base) : null;
+ if (resolved != null) {
+ return resolved;
+ }
throw new TransformerException(forbiddenMessage("uri", null, null, href, base));
- };
+ }
+ }
+
+ /**
+ * {@link XMLResolver} floor: consults an optional caller-supplied resolver and denies (throws) whatever the caller does not resolve.
+ *
+ * The StAX counterpart of {@link FallbackDenyResolver}, installed on each entity-resolution hook. The hardened {@link javax.xml.stream.XMLInputFactory}
+ * wrapper routes a caller-set resolver through {@link #setDelegate} rather than letting it replace the floor. A caller opts a specific entity in by returning
+ * a non-{@code null} result; anything left unresolved goes to {@link #onUnresolved}, which denies by default. Subclasses override {@code onUnresolved} to give
+ * a hook a different unresolved policy (e.g. return an empty input for the external DTD subset, or for undeclared entities) while keeping the caller-delegate
+ * behavior.
+ */
+ static class FallbackDenyXMLResolver implements XMLResolver {
+
+ private XMLResolver delegate;
+
+ FallbackDenyXMLResolver(final XMLResolver delegate) {
+ this.delegate = delegate;
+ }
+
+ final void setDelegate(final XMLResolver delegate) {
+ this.delegate = delegate;
+ }
+
+ final XMLResolver getDelegate() {
+ return delegate;
+ }
+
+ @Override
+ public final Object resolveEntity(final String publicID, final String systemID, final String baseURI, final String namespace) throws XMLStreamException {
+ final Object resolved = delegate != null ? delegate.resolveEntity(publicID, systemID, baseURI, namespace) : null;
+ return resolved != null ? resolved : onUnresolved(publicID, systemID, baseURI, namespace);
+ }
/**
- * Refuses every external entity lookup performed by a StAX parser.
+ * Outcome when the caller delegate does not resolve the entity. Denies by default; a subclass may return an {@link java.io.InputStream}, {@link Source} or
+ * other {@link XMLResolver}-supported value (for example an empty input) instead of calling {@code super}, or {@code throw}
+ * {@link #denied(String, String, String, String)} to deny only some lookups.
+ *
+ * @param publicID the public identifier, or {@code null} if none.
+ * @param systemID the system identifier of the unresolved entity.
+ * @param baseURI the base URI for relative resolution, or {@code null}.
+ * @param namespace the namespace (or, for Woodstox, the entity name), or {@code null}.
+ * @return the replacement input, or a value the caller's parser accepts; the default implementation never returns normally.
+ * @throws XMLStreamException to deny the lookup (the default behavior).
*/
- static final XMLResolver XML = (publicID, systemID, baseURI, namespace) -> {
- throw new XMLStreamException(forbiddenMessage(null, namespace, publicID, systemID, baseURI));
- };
+ protected Object onUnresolved(final String publicID, final String systemID, final String baseURI, final String namespace) throws XMLStreamException {
+ throw denied(publicID, systemID, baseURI, namespace);
+ }
- private DenyAll() {
+ /**
+ * Builds the standard "forbidden by hardening" exception for a denied lookup, so a subclass with a mixed policy can reuse the deny outcome for the
+ * lookups it refuses.
+ *
+ * @param publicID the public identifier, or {@code null} if none.
+ * @param systemID the system identifier of the unresolved entity.
+ * @param baseURI the base URI for relative resolution, or {@code null}.
+ * @param namespace the namespace (or, for Woodstox, the entity name), or {@code null}.
+ * @return the exception to throw.
+ */
+ protected final XMLStreamException denied(final String publicID, final String systemID, final String baseURI, final String namespace) {
+ return new XMLStreamException(forbiddenMessage(null, namespace, publicID, systemID, baseURI));
}
}
/**
- * Returns an empty input for every external resource lookup so the parse can continue without replacement content.
- *
- * Only an {@link XMLResolver} flavour is exposed: schema and XSLT compile paths must always deny imports, and SAX/DOM use the deny-all hooks.
+ * {@link FallbackDenyXMLResolver} variant whose unresolved policy returns an empty input instead of throwing, so the parse continues with no replacement
+ * content. Used on Woodstox's DTD-subset and undeclared-entity hooks (where a missing resource must be skipped, not denied), while still consulting an
+ * optional caller-supplied resolver first.
*/
- static final class IgnoreAll {
+ static class FallbackIgnoreXMLResolver extends FallbackDenyXMLResolver {
/**
- * Empty {@link ByteArrayInputStream} shared across every call. {@code read()} on a zero-length array always returns {@code -1}, so reusing the
- * instance is safe even though the type is technically stateful.
+ * Empty {@link ByteArrayInputStream} shared across every call. {@code read()} on a zero-length array always returns {@code -1}, so reusing the instance
+ * is safe even though the type is technically stateful.
*/
private static final InputStream EMPTY = new ByteArrayInputStream(new byte[0]);
- /**
- * Returns an empty input for every external entity lookup performed by a StAX parser.
- */
- static final XMLResolver XML = (publicID, systemID, baseURI, namespace) -> EMPTY;
+ FallbackIgnoreXMLResolver(final XMLResolver delegate) {
+ super(delegate);
+ }
+
+ @Override
+ protected Object onUnresolved(final String publicID, final String systemID, final String baseURI, final String namespace) throws XMLStreamException {
+ return EMPTY;
+ }
+ }
- private IgnoreAll() {
+ /**
+ * Resolves {@code systemId} against {@code baseURI}.
+ *
+ * @param baseURI the absolute base URI to resolve against, or {@code null} if none is available.
+ * @param systemId the system identifier, possibly relative to {@code baseURI}.
+ * @return the absolutized system identifier, or {@code systemId} unchanged when it cannot or need not be resolved.
+ */
+ private static String absolutize(final String baseURI, final String systemId) {
+ if (systemId == null || baseURI == null) {
+ return systemId;
+ }
+ try {
+ final URI system = new URI(systemId);
+ return system.isAbsolute() ? systemId : new URI(baseURI).resolve(system).toString();
+ } catch (final URISyntaxException e) {
+ return systemId;
}
}
diff --git a/src/main/java/org/apache/commons/xml/SAXParserHardener.java b/src/main/java/org/apache/commons/xml/SAXParserHardener.java
index 7b1fb7d..47e952e 100644
--- a/src/main/java/org/apache/commons/xml/SAXParserHardener.java
+++ b/src/main/java/org/apache/commons/xml/SAXParserHardener.java
@@ -21,6 +21,7 @@
import static org.apache.commons.xml.JaxpSetters.setOptionalFeature;
import static org.apache.commons.xml.JaxpSetters.trySetProperty;
+import java.io.IOException;
import java.util.Objects;
import javax.xml.XMLConstants;
@@ -32,7 +33,6 @@
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.XMLReader;
-import org.xml.sax.ext.DefaultHandler2;
import org.xml.sax.ext.LexicalHandler;
/**
@@ -45,7 +45,7 @@
* Android (Harmony / Expat): {@link XMLConstants#FEATURE_SECURE_PROCESSING FSP} and the JAXP 1.5 {@code ACCESS_EXTERNAL_*} properties
* are not recognized, and libexpat enforces its own Billion Laughs check, so neither is applied. Two fixups are still needed: a subset-aware deny-all
* resolver (Expat ignores external fetches silently when no resolver is set, so an explicit one is required to fail on external entities while
- * still letting an unused external subset load), and an {@link ExpatReaderWrapper} so the unsupported {@code namespace-prefixes} feature is rejected at
+ * still letting an unused external subset load), and a {@link HardeningExpatXMLReader} so the unsupported {@code namespace-prefixes} feature is rejected at
* configuration time rather than mid-parse.
* FSP: required on every other reader. It switches on the implementation's built-in security manager, which is what carries the
* processing limits.
@@ -54,65 +54,69 @@
* Limits: applied best-effort by {@link Limits#tryApply(XMLReader)}, which adapts to the JDK limit properties or Xerces'
* {@code SecurityManager} as appropriate.
* {@code ACCESS_EXTERNAL_*}: the dividing capability. Readers that honor it (the JDK-internal Xerces) block external fetches through
- * the JAXP 1.5 properties and are returned as-is. Readers that reject it (the external Xerces distribution) get a deny-all {@link EntityResolver}
- * installed instead.
+ * the JAXP 1.5 properties and are returned as-is. Readers that reject it (the external Xerces distribution) are wrapped in a {@link HardeningXMLReader}
+ * that keeps a deny-all {@link EntityResolver} floor a caller-set resolver cannot remove.
*
*/
final class SAXParserHardener {
/**
- * Resolver that denies every external resource lookup an {@link XMLReader} attempts, except the external DTD subset declared by the DOCTYPE.
+ * Deny floor that additionally lets the external DTD subset declared by the DOCTYPE be skipped silently; merely declaring an external subset does
+ * not throw.
*
* Android's Expat routes every external fetch (subset, DOCTYPE {@code SYSTEM}, general/parameter entity) through the 2-arg
* {@link EntityResolver#resolveEntity(String, String)}; a deny-all resolver there would also reject a DOCTYPE that merely names an unused external
- * subset. Tracking the subset's identifiers through the {@link LexicalHandler} DTD events lets this resolver allow exactly that one lookup and throw on
- * everything else. It is stateful, so a fresh instance is installed per reader.
+ * subset. As a {@link Resolvers.FallbackDenyResolver} it consults the caller's resolver first; as a {@link LexicalHandler} (via {@code DefaultHandler2}) it
+ * tracks the declared subset's identifiers so {@link #onUnresolved} can tell the subset apart from a forbidden external general or parameter entity. It is
+ * stateful, so a fresh instance is installed per reader.
*/
- private static final class DtdAwareDenyResolver extends DefaultHandler2 {
-
- private static String forbiddenMessage(final String publicId, final String systemId) {
- return String.format("External Entity: failed to read external entity (publicId='%s', systemId='%s'); external entity access is denied.",
- publicId, systemId);
- }
+ private static final class DtdAwareDenyResolver extends Resolvers.FallbackDenyResolver {
private String dtdPublicId;
private String dtdSystemId;
private boolean inDtd;
+ DtdAwareDenyResolver() {
+ super(null);
+ }
+
+ @Override
+ public void startDTD(final String name, final String publicId, final String systemId) {
+ inDtd = true;
+ dtdPublicId = publicId;
+ dtdSystemId = systemId;
+ }
+
@Override
public void endDTD() {
inDtd = false;
}
@Override
- public InputSource resolveEntity(final String publicId, final String systemId) throws SAXException {
+ protected InputSource onUnresolved(final String name, final String publicId, final String baseURI, final String systemId)
+ throws SAXException, IOException {
+ // Declaring (but not using) an external subset must not throw: let the parser skip it silently. Everything else is denied by the floor.
if (inDtd && Objects.equals(publicId, dtdPublicId) && Objects.equals(systemId, dtdSystemId)) {
return null;
}
- throw new SAXException(forbiddenMessage(publicId, systemId));
- }
-
- @Override
- public void startDTD(final String name, final String publicId, final String systemId) {
- inDtd = true;
- dtdPublicId = publicId;
- dtdSystemId = systemId;
+ return super.onUnresolved(name, publicId, baseURI, systemId);
}
}
/**
- * Wrapper around Android's {@code org.apache.harmony.xml.ExpatReader} that surfaces its {@code namespace-prefixes} limitation at configuration time.
+ * {@link HardeningXMLReader} for Android's {@code org.apache.harmony.xml.ExpatReader} that additionally surfaces its {@code namespace-prefixes} limitation at
+ * configuration time.
*
* ExpatReader does not actually support the {@code namespace-prefixes} feature: enabling it is accepted by {@code setFeature} but fails later, during
* {@code parse}, with a {@link SAXNotSupportedException}. Reporting the rejection eagerly from {@link #setFeature(String, boolean)} lets consumers that probe
* the feature, such as Xalan's identity transformer, catch the exception and fall back instead of failing the whole parse.
*/
- static final class ExpatReaderWrapper extends DelegatingXMLReader {
+ static final class HardeningExpatXMLReader extends HardeningXMLReader {
private static final String NAMESPACE_PREFIXES_FEATURE = "http://xml.org/sax/features/namespace-prefixes";
- ExpatReaderWrapper(final XMLReader delegate) {
- super(delegate);
+ HardeningExpatXMLReader(final XMLReader delegate, final Resolvers.FallbackDenyResolver floor) {
+ super(delegate, floor);
}
@Override
@@ -153,18 +157,19 @@ static SAXParserFactory harden(final SAXParserFactory factory) {
* @throws IllegalStateException if a required hardening setting cannot be applied to the underlying implementation.
*/
static XMLReader hardenReader(final XMLReader reader) {
- if (reader instanceof ExpatReaderWrapper) {
- // Already hardened (e.g. handed back through XmlFactories.harden(XMLReader)); applying the Expat fixups again would be redundant.
+ if (reader instanceof HardeningXMLReader) {
+ // Already hardened (e.g. handed back through XmlFactories.harden(XMLReader)); the floor is already in place.
return reader;
}
if (ANDROID_EXPAT_READER.equals(reader.getClass().getName())) {
- // Expat ignores external fetches when no resolver is set; install one that fails on external entities but lets an unused external subset load.
- final DtdAwareDenyResolver resolver = new DtdAwareDenyResolver();
- reader.setEntityResolver(resolver);
- // The resolver needs the DTD-boundary events to tell the subset apart from entities; Expat recognizes the lexical-handler property.
- trySetProperty(reader, LEXICAL_HANDLER_PROPERTY, resolver);
- // Reject the unsupported namespace-prefixes feature eagerly rather than mid-parse.
- return new ExpatReaderWrapper(reader);
+ // Expat ignores external fetches when no resolver is set; a subset-aware deny floor fails on external entities but lets an unused subset load.
+ // HardeningExpatXMLReader keeps that floor non-bypassable (routing a caller-set resolver, including SAXParser.parse's handler, through it) and rejects
+ // the unsupported namespace-prefixes feature eagerly rather than mid-parse.
+ final DtdAwareDenyResolver floor = new DtdAwareDenyResolver();
+ final HardeningExpatXMLReader hardened = new HardeningExpatXMLReader(reader, floor);
+ // The floor needs the DTD-boundary events to tell the subset apart from entities; Expat recognizes the lexical-handler property.
+ trySetProperty(hardened, LEXICAL_HANDLER_PROPERTY, floor);
+ return hardened;
}
// Required: enables the JDK XMLSecurityManager / Xerces SecurityManager limits.
setFeature(reader, XMLConstants.FEATURE_SECURE_PROCESSING, true);
@@ -175,12 +180,12 @@ static XMLReader hardenReader(final XMLReader reader) {
// ACCESS_EXTERNAL_* support is the dividing capability between JAXP 1.5 implementations and older ones.
if (trySetProperty(reader, XMLConstants.ACCESS_EXTERNAL_DTD, "")
&& trySetProperty(reader, XMLConstants.ACCESS_EXTERNAL_SCHEMA, "")) {
- // Honoured (stock JDK): the JAXP 1.5 properties block external fetches, so the bare reader is already hardened.
+ // Honored (stock JDK): the JAXP 1.5 properties block external fetches, so the bare reader is already hardened.
return reader;
}
- // Rejected: external Xerces ignores ACCESS_EXTERNAL_*; install a deny-all resolver, the only block.
- reader.setEntityResolver(Resolvers.DenyAll.ENTITY2);
- return reader;
+ // Rejected: external Xerces ignores ACCESS_EXTERNAL_*; wrap the reader so a deny-all resolver floor blocks external fetches and a caller-set resolver
+ // cannot replace it.
+ return new HardeningXMLReader(reader);
}
private SAXParserHardener() {
diff --git a/src/main/java/org/apache/commons/xml/StaxHardener.java b/src/main/java/org/apache/commons/xml/StaxHardener.java
index dc0241f..bd83e76 100644
--- a/src/main/java/org/apache/commons/xml/StaxHardener.java
+++ b/src/main/java/org/apache/commons/xml/StaxHardener.java
@@ -21,7 +21,6 @@
import static org.apache.commons.xml.JaxpSetters.trySetProperty;
import javax.xml.stream.XMLInputFactory;
-import javax.xml.stream.XMLResolver;
import javax.xml.stream.XMLStreamException;
/**
@@ -34,42 +33,53 @@
* each implementation honors its own and rejects the other's.
* External DTD subset: skipped via Zephyr's {@value #ZEPHYR_IGNORE_EXTERNAL_DTD} (best-effort), so a DOCTYPE-only document parses
* without a fetch attempt instead of tripping the deny-all resolver below. Woodstox skips it through {@value #WSTX_DTD_RESOLVER} instead.
- * External entities: denied through resolvers, leaving the standard {@code SUPPORT_DTD} / {@code IS_SUPPORTING_EXTERNAL_ENTITIES}
- * defaults untouched. Woodstox exposes fine-grained hooks, so when all three apply the factory is Woodstox: {@value #WSTX_DTD_RESOLVER} (empty external
- * subset, but a thrown error on external parameter entities, which share that hook), {@value #WSTX_ENTITY_RESOLVER} (throw on declared external general
- * entities) and {@value #WSTX_UNDECLARED_ENTITY_RESOLVER} (silently drop undeclared references left by the skipped subset). Any factory that does not
- * accept that trio (the JDK Zephyr, or an unrecognized implementation) instead gets a single deny-all {@link Resolvers.DenyAll#XML} through
- * {@code setXMLResolver}.
+ * External entities: denied through a non-removable {@link Resolvers.FallbackDenyXMLResolver} floor on the entity-resolution hook,
+ * leaving the standard {@code SUPPORT_DTD} / {@code IS_SUPPORTING_EXTERNAL_ENTITIES} defaults untouched. Woodstox exposes fine-grained hooks, so when all
+ * three apply the factory is Woodstox: {@value #WSTX_DTD_RESOLVER} (empty external subset, but a thrown error on external parameter entities, which share
+ * that hook), {@value #WSTX_ENTITY_RESOLVER} (the floor, denying declared external general entities) and {@value #WSTX_UNDECLARED_ENTITY_RESOLVER}
+ * (silently drop undeclared references left by the skipped subset). Any factory that does not accept that trio (the JDK Zephyr, or an unrecognized
+ * implementation) instead gets the floor through {@code setXMLResolver}. Either way the factory is wrapped in a {@link HardeningXMLInputFactory} that
+ * routes a caller-set resolver through the floor rather than letting it replace the deny-all block.
*
*/
final class StaxHardener {
- /** Zephyr property: skip external DTD subset loading entirely, so a DOCTYPE-only document parses without a fetch attempt. */
- private static final String ZEPHYR_IGNORE_EXTERNAL_DTD = "http://java.sun.com/xml/stream/properties/ignore-external-dtd";
-
/** Woodstox property: resolver consulted for the external DTD subset. */
- private static final String WSTX_DTD_RESOLVER = "com.ctc.wstx.dtdResolver";
+ static final String WSTX_DTD_RESOLVER = "com.ctc.wstx.dtdResolver";
/** Woodstox property: resolver consulted for declared external general entities. */
- private static final String WSTX_ENTITY_RESOLVER = "com.ctc.wstx.entityResolver";
+ static final String WSTX_ENTITY_RESOLVER = "com.ctc.wstx.entityResolver";
/** Woodstox property: resolver consulted for undeclared entity references. */
- private static final String WSTX_UNDECLARED_ENTITY_RESOLVER = "com.ctc.wstx.undeclaredEntityResolver";
+ static final String WSTX_UNDECLARED_ENTITY_RESOLVER = "com.ctc.wstx.undeclaredEntityResolver";
+
+ /** Zephyr property: skip external DTD subset loading entirely, so a DOCTYPE-only document parses without a fetch attempt. */
+ private static final String ZEPHYR_IGNORE_EXTERNAL_DTD = "http://java.sun.com/xml/stream/properties/ignore-external-dtd";
/**
- * Hybrid Woodstox DTD resolver: returns the empty input for the external DTD subset, throws on external parameter entities.
+ * Woodstox DTD-subset floor: a {@link Resolvers.FallbackIgnoreXMLResolver} that returns the empty input for the external DTD subset (its inherited policy)
+ * but throws on external parameter entities.
*
- * Woodstox calls this hook with {@code entityName == null} for the subset and {@code entityName != null} for parameter-entity expansion; that
- * discriminator is Woodstox-specific (the JDK Zephyr's {@code XMLResolver} always receives {@code null} as the 4th argument), so the resolver lives
- * here and is applied best-effort, ignored by implementations that do not recognize the property.
+ * Woodstox calls this hook with {@code entityName == null} for the subset and {@code entityName != null} for parameter-entity expansion (that
+ * discriminator is the 4th {@code resolveEntity} argument; the JDK Zephyr always passes {@code null} there). Applied best-effort, ignored by implementations
+ * that do not recognize the property.
*/
- static final XMLResolver DTD_SUBSET_ONLY = (publicID, systemID, baseURI, entityName) -> {
- if (entityName != null) {
- throw new XMLStreamException("External parameter entity '" + entityName + "' refused (publicID=" + publicID + ", systemID=" + systemID
- + ", baseURI=" + baseURI + ")");
+ private static final class DtdSubsetFloor extends Resolvers.FallbackIgnoreXMLResolver {
+
+ DtdSubsetFloor() {
+ super(null);
}
- return Resolvers.IgnoreAll.XML.resolveEntity(publicID, systemID, baseURI, entityName);
- };
+
+ @Override
+ protected Object onUnresolved(final String publicID, final String systemID, final String baseURI, final String entityName) throws XMLStreamException {
+ // External parameter entity (entityName != null): deny, reusing the standard hardening message.
+ if (entityName != null) {
+ throw denied(publicID, systemID, baseURI, entityName);
+ }
+ // Subset (entityName == null): skip it with the empty input from the ignore floor.
+ return super.onUnresolved(publicID, systemID, baseURI, entityName);
+ }
+ }
static XMLInputFactory harden(final XMLInputFactory factory) {
// Optional, implementation-based: JDK limit properties or Woodstox limit properties.
@@ -77,14 +87,16 @@ static XMLInputFactory harden(final XMLInputFactory factory) {
// Optional: Zephyr's StAX equivalent of XERCES_LOAD_EXTERNAL_DTD=false skips the external DTD subset entirely.
setOptionalProperty(factory, ZEPHYR_IGNORE_EXTERNAL_DTD, true);
- // Woodstox-specific fine-grained resolvers
- if (!(trySetProperty(factory, WSTX_DTD_RESOLVER, DTD_SUBSET_ONLY)
- && trySetProperty(factory, WSTX_ENTITY_RESOLVER, Resolvers.DenyAll.XML)
- && trySetProperty(factory, WSTX_UNDECLARED_ENTITY_RESOLVER, Resolvers.IgnoreAll.XML))) {
- // Fallback: use deny-all resolver
- factory.setXMLResolver(Resolvers.DenyAll.XML);
+ // Each hook carries its own FallbackDenyXMLResolver floor; a caller can opt specific entities in through it, but cannot remove it (see
+ // HardeningXMLInputFactory, which routes a caller-set resolver into the floor rather than replacing it). The DTD-subset and undeclared-entity hooks skip
+ // (empty input) rather than deny on an unresolved lookup, so a DOCTYPE-only document still parses.
+ if (!(trySetProperty(factory, WSTX_DTD_RESOLVER, new DtdSubsetFloor())
+ && trySetProperty(factory, WSTX_ENTITY_RESOLVER, new Resolvers.FallbackDenyXMLResolver(null))
+ && trySetProperty(factory, WSTX_UNDECLARED_ENTITY_RESOLVER, new Resolvers.FallbackIgnoreXMLResolver(null)))) {
+ // Fallback (JDK Zephyr or unrecognized): the single resolver carries the deny-all floor.
+ factory.setXMLResolver(new Resolvers.FallbackDenyXMLResolver(null));
}
- return factory;
+ return new HardeningXMLInputFactory(factory);
}
private StaxHardener() {
diff --git a/src/main/java/org/apache/commons/xml/TransformerHardener.java b/src/main/java/org/apache/commons/xml/TransformerHardener.java
index ae052b7..d3c5a6b 100644
--- a/src/main/java/org/apache/commons/xml/TransformerHardener.java
+++ b/src/main/java/org/apache/commons/xml/TransformerHardener.java
@@ -50,19 +50,15 @@
* permissive default re-opens the external-DTD/entity channel during stylesheet compilation. Xalan rejects the attribute (best-effort, ignored) and closes
* that channel through the hardened reader instead. Its sibling {@code ACCESS_EXTERNAL_STYLESHEET} is not set: the deny-all resolver below already
* guards the only channel it covers.
- * {@link Resolvers.DenyAll#URI}: required. A deny-all {@link URIResolver} blocks {@code xsl:import}/{@code xsl:include} at compile time
- * and {@code document()} at runtime, the one channel both XSLTC and Xalan route through.
+ * {@link Resolvers.FallbackDenyURIResolver} floor: required. A deny-all {@link URIResolver} floor, installed by
+ * {@link HardeningTransformerFactory} and carried onto every produced {@link Transformer}, blocks {@code xsl:import}/{@code xsl:include} at compile time
+ * and {@code document()} at runtime, the one channel both XSLTC and Xalan route through. A caller-set {@link URIResolver} is routed through the floor
+ * rather than replacing it, so a caller can opt a specific URI in but cannot drop the block.
* {@link HardeningTransformerFactory}: required. Both implementations fall back to {@code SAXParserFactory.newInstance()} to parse a
* stylesheet or source document that does not carry its own reader, and only set FSP on it; wrapping the factory rewrites every {@link Source} through an
* {@link XmlFactories}-hardened reader instead. On Xalan that reader (its deny-all {@link org.xml.sax.EntityResolver} or its own
* {@code ACCESS_EXTERNAL_DTD}) is what blocks external DTDs and entities.
*
- *
- * Caveats
- *
- * - Replacing the {@code URIResolver} on the returned factory or on a produced transformer cancels the deny-all block for XSLT URI fetches; neither XSLTC
- * nor Xalan exposes a tamper-resistant equivalent of JAXP 1.5 {@code ACCESS_EXTERNAL_STYLESHEET}.
- *
*/
final class TransformerHardener {
@@ -88,9 +84,8 @@ static TransformerFactory harden(final TransformerFactory factory) {
// A permissive default here would re-open the external-DTD/entity channel.
// Xalan rejects the attribute and blocks that channel through a deny-all resolver instead.
setOptionalAttribute(factory, XMLConstants.ACCESS_EXTERNAL_DTD, "");
- // Required: the one channel both implementations honor; blocks xsl:import/include at compile time and document() at runtime.
- factory.setURIResolver(Resolvers.DenyAll.URI);
- // Required: source/stylesheet parsing provisions its own SAX reader otherwise; the wrapper routes every Source through a hardened one.
+ // Required: source/stylesheet parsing provisions its own SAX reader otherwise; the wrapper routes every Source through a hardened one and installs the
+ // deny-all URIResolver floor (blocking xsl:import/include at compile time and document() at runtime) that a caller-set resolver cannot remove.
return new HardeningTransformerFactory((SAXTransformerFactory) factory);
}
diff --git a/src/site/markdown/threat_model.md b/src/site/markdown/threat_model.md
index 695bf7b..af84911 100644
--- a/src/site/markdown/threat_model.md
+++ b/src/site/markdown/threat_model.md
@@ -101,9 +101,6 @@ they govern external resource access, DTD, entity or schema handling, the instal
limits; loosening any of them, on the returned factory or on a parser, reader, transformer, validator or schema it
produces, breaks the hardening for that instance.
-- `com.ctc.wstx.dtdResolver`
-- `com.ctc.wstx.entityResolver`
-- `com.ctc.wstx.undeclaredEntityResolver`
- `http://apache.org/xml/features/disallow-doctype-decl`
- `http://apache.org/xml/features/nonvalidating/load-external-dtd`
- `http://apache.org/xml/properties/internal/entity-resolver`
@@ -120,16 +117,38 @@ produces, breaks the hardening for that instance.
- `jdk.xml.overrideDefaultParser`
- the JDK processing-limit properties listed above
-This list is not exhaustive: any other feature, attribute, property or system property that grants access to an external
-resource, relaxes DTD or entity processing, installs a resolver, or raises a processing limit is reserved on the same
-terms. Installing a resolver through the typed `set*Resolver` methods, or through the `DefaultHandler` passed to
-`SAXParser.parse`, has the same effect (see [What is out of scope](#what-is-out-of-scope)).
+This list is not exhaustive:
+any other feature, attribute, property, or system property that
+grants access to an external resource,
+relaxes DTD or entity processing,
+installs a resolver the hardening layer does not wrap
+(like the Xerces-specific `http://apache.org/xml/properties/internal/entity-resolver`, listed above),
+or raises a processing limit
+is reserved on the same terms.
+
+Installing a resolver through the typed `set*Resolver` methods, the `DefaultHandler` passed to `SAXParser.parse`, or the resolver properties listed under **Settings you may modify** does not loosen the hardening:
+those paths are wrapped by a non-removable floor.
**Settings you may modify**
The following are security-relevant but safe to change on a returned factory: the protection they appear to govern is
enforced by the reserved settings above, which a caller cannot lift.
+- **Resolvers.** You may install your own resolver: the hardening floor wraps it instead of being replaced, so it stays
+ in force. This covers the typed setters and the resolver properties:
+ - `setEntityResolver(...)` (DOM and SAX), including the `DefaultHandler` passed to `SAXParser.parse(..., DefaultHandler)`,
+ - `setResourceResolver(...)` (schema compilation and validation),
+ - `setURIResolver(...)` (XSLT),
+ - `setXMLResolver(...)` and the equivalent StAX resolver properties:
+ - `com.ctc.wstx.dtdResolver`,
+ - `com.ctc.wstx.entityResolver`,
+ - `com.ctc.wstx.undeclaredEntityResolver`,
+ - `javax.xml.stream.resolver`.
+
+ Your resolver is consulted first, but the floor denies or ignores whatever it leaves unresolved.
+ It therefore *must* resolve every resource you need available: a `null` return blocks the lookup,
+ it does not fall through to a fetch.
+
- **Validation.** You may turn on DTD or XSD validation, using these methods and features/properties:
- `setSchema(Schema)`,
- `setValidating(true)`,
@@ -156,9 +175,10 @@ and reports against a factory reconfigured in any of the ways below are out of s
- **Modifying a reserved setting.** Loosening any feature, attribute or property reserved under
[Assumptions about the environment](#assumptions-about-the-environment).
-- **Installing your own resolver.** Setting an entity, resource or URI resolver, whether it returns `null` or returns
- content, replaces the resolution policy the hardening relies on. This includes the `DefaultHandler` passed to
- `SAXParser.parse(..., DefaultHandler)`, which the parser installs as its entity resolver.
+- **A resolver that resolves untrusted resources.** Installing a resolver does not lift the floor (see
+ **Settings you may modify** above), but your resolver is consulted ahead of it, so any resource it resolves (returns
+ content for) is fetched, including one named by an untrusted identifier. Which resources it resolves is your policy to
+ enforce.
- **Caller-supplied top-level URIs.** A URI passed directly to a parse call (`DocumentBuilder.parse(String)`,
`StreamSource(systemId)`, a `SAXSource` built from a system id) is fetched as-is by the JAXP implementation without
consulting the hardening layer. Restrict it yourself if the URI is untrusted.
diff --git a/src/test/java/org/apache/commons/xml/AttackTestSupport.java b/src/test/java/org/apache/commons/xml/AttackTestSupport.java
index 9b6c07f..952cd75 100644
--- a/src/test/java/org/apache/commons/xml/AttackTestSupport.java
+++ b/src/test/java/org/apache/commons/xml/AttackTestSupport.java
@@ -40,6 +40,7 @@
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
+import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.api.function.ThrowingSupplier;
import org.w3c.dom.Document;
@@ -47,9 +48,12 @@
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
+import org.xml.sax.SAXNotRecognizedException;
+import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
+import org.xml.sax.helpers.XMLFilterImpl;
/**
* Shared fixtures for attack tests.
@@ -98,7 +102,7 @@ final class AttackTestSupport {
* error or fatalError so the helpers can observe the block via the same mechanism the spec uses to surface it. Warnings stay silent: they are not security
* signals.
*/
- private static final class StrictReporter implements ErrorListener, ErrorHandler {
+ static final class StrictReporter implements ErrorListener, ErrorHandler {
@Override
public void error(final SAXParseException exception) throws SAXException {
@@ -170,7 +174,7 @@ public void warning(final TransformerException exception) {
* presence is the leak signal.
*/
static final String LEAKED_MARKER = "All your base are belong to us";
- private static final StrictReporter STRICT_REPORTER = new StrictReporter();
+ static final StrictReporter STRICT_REPORTER = new StrictReporter();
/**
* Asserts a hardened DOM parse of the payload throws.
@@ -616,6 +620,18 @@ static void assertXmlReaderParses(final String payload) {
assertParseSucceeds(() -> consumeXmlReader(rawHardenedReader(), payload), "XMLReader");
}
+ /**
+ * Runs the action and, if it throws, aborts (skips rather than fails) the calling test. Used to guard platform-optional configuration such as
+ * {@code setXIncludeAware}, which the Android JAXP implementations do not support.
+ */
+ static void assumeDoesNotThrow(final Executable action) {
+ try {
+ action.execute();
+ } catch (final Throwable t) {
+ Assumptions.assumeTrue(false, "platform does not support this configuration: " + t);
+ }
+ }
+
/**
* Builds the failure message used by every {@code assert*Blocks(...)} helper.
*
@@ -732,7 +748,31 @@ private static SAXSource permissiveSaxSource(final String xml) {
factory.setNamespaceAware(true);
final XMLReader reader = strictXMLReader(factory);
suppressException(() -> reader.setProperty(JDK_ENTITY_EXPANSION_LIMIT, "0"));
- return new SAXSource(IS_ANDROID ? new SAXParserHardener.ExpatReaderWrapper(reader) : reader, new InputSource(new StringReader(xml)));
+ // On Android the reader is Expat, which accepts namespace-prefixes at setFeature time but fails mid-parse; a permissive TrAX identity transform probes
+ // that feature, so wrap it to reject the feature eagerly (matching the production HardeningExpatXMLReader) while keeping the control permissive (no floor).
+ return new SAXSource(IS_ANDROID ? new PermissiveExpatReader(reader) : reader, new InputSource(new StringReader(xml)));
+ }
+
+ /**
+ * Test-only permissive counterpart of {@code SAXParserHardener.HardeningExpatXMLReader}: a pass-through Expat wrapper that rejects the
+ * {@code namespace-prefixes} feature eagerly (so a probing TrAX identity transformer falls back instead of failing the whole parse) but installs no deny-all
+ * resolver floor, so the unconfigured/positive controls stay permissive.
+ */
+ private static final class PermissiveExpatReader extends XMLFilterImpl {
+
+ private static final String NAMESPACE_PREFIXES_FEATURE = "http://xml.org/sax/features/namespace-prefixes";
+
+ PermissiveExpatReader(final XMLReader parent) {
+ super(parent);
+ }
+
+ @Override
+ public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException {
+ if (value && NAMESPACE_PREFIXES_FEATURE.equals(name)) {
+ throw new SAXNotSupportedException("ExpatReader does not support enabling the '" + NAMESPACE_PREFIXES_FEATURE + "' feature");
+ }
+ super.setFeature(name, value);
+ }
}
private static boolean probeAndroid() {
@@ -790,7 +830,7 @@ static StreamSource streamSource(final String xml) {
/**
* Builds a {@link DocumentBuilder} from {@code factory} with {@link #STRICT_REPORTER} installed as its error handler.
*/
- private static DocumentBuilder strictDocumentBuilder(final DocumentBuilderFactory factory) throws ParserConfigurationException {
+ static DocumentBuilder strictDocumentBuilder(final DocumentBuilderFactory factory) throws ParserConfigurationException {
final DocumentBuilder builder = factory.newDocumentBuilder();
builder.setErrorHandler(STRICT_REPORTER);
return builder;
diff --git a/src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java b/src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java
new file mode 100644
index 0000000..42a473f
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java
@@ -0,0 +1,376 @@
+/*
+ * 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
+ *
+ * https://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.commons.xml;
+
+import static org.apache.commons.xml.AttackTestSupport.assertParseFails;
+import static org.apache.commons.xml.AttackTestSupport.assertParseSucceeds;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.StringReader;
+import java.net.URL;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLResolver;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.URIResolver;
+import javax.xml.validation.SchemaFactory;
+
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.bootstrap.DOMImplementationRegistry;
+import org.w3c.dom.ls.DOMImplementationLS;
+import org.w3c.dom.ls.LSInput;
+import org.w3c.dom.ls.LSResourceResolver;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Checks that a caller-supplied resolver cannot remove the hardened deny-all floor on any factory.
+ *
+ * The observable contract on every hardened factory is the same: a resource the caller resolves (returns a non-null value) is allowed, but anything the
+ * caller does not resolve is denied instead of fetched, so a resolver that resolves nothing leaves the block in place. Most factories enforce this with a
+ * {@link Resolvers.FallbackDenyResolver}-style floor that consults the caller and denies on a {@code null} return; Saxon enforces the equivalent through its
+ * {@code ALLOWED_PROTOCOLS} restrictor. Every resolver channel is exercised: the SAX/DOM {@link EntityResolver}, the StAX {@link XMLResolver}, the schema
+ * {@link LSResourceResolver} and the XSLT {@link URIResolver}.
+ */
+class EntityResolverFloorTest {
+
+ /** systemId the allow-list resolvers permit (its content carries {@link AttackTestSupport#LEAKED_MARKER}). */
+ private static final String ALLOWED = AttackTestSupport.resourceUrl("referenced.txt").toString();
+
+ /** systemId the allow-list resolvers do not resolve (and the floor must deny). */
+ private static final String UNLISTED = AttackTestSupport.resourceUrl("referenced.xml").toString();
+
+ // ---- Entity channel (DOM / SAX) ----------------------------------------------------------------------------------------------------------------------
+
+ /** Resolves only {@link #ALLOWED}; returns {@code null} for anything else. */
+ private static final EntityResolver ENTITY_ALLOW_LIST = (publicId, systemId) ->
+ ALLOWED.equals(systemId) ? new InputSource(new URL(systemId).openStream()) : null;
+
+ private static String entityPayload(final String entitySystemId) {
+ return "\n"
+ + "\n]>\n"
+ + "&xxe;";
+ }
+
+ private static DocumentBuilder hardenedBuilder() throws Exception {
+ final DocumentBuilder builder = XmlFactories.newDocumentBuilderFactory().newDocumentBuilder();
+ builder.setErrorHandler(AttackTestSupport.STRICT_REPORTER);
+ return builder;
+ }
+
+ private static XMLReader hardenedReader() throws Exception {
+ final XMLReader reader = XmlFactories.newSAXParserFactory().newSAXParser().getXMLReader();
+ reader.setErrorHandler(AttackTestSupport.STRICT_REPORTER);
+ return reader;
+ }
+
+ @Test
+ @Tag("dom")
+ void domResolvesAllowListed() throws Exception {
+ Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES, "platform DOM does not resolve user-defined entities");
+ final DocumentBuilder builder = hardenedBuilder();
+ builder.setEntityResolver(ENTITY_ALLOW_LIST);
+ final Document doc = builder.parse(AttackTestSupport.inputSource(entityPayload(ALLOWED)));
+ assertTrue(doc.getDocumentElement().getTextContent().contains(AttackTestSupport.LEAKED_MARKER),
+ "allow-listed external entity should resolve through the caller's resolver");
+ }
+
+ @Test
+ @Tag("dom")
+ void domDeniesUnlisted() throws Exception {
+ Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES, "platform DOM does not resolve user-defined entities");
+ final DocumentBuilder builder = hardenedBuilder();
+ builder.setEntityResolver(ENTITY_ALLOW_LIST);
+ assertThrows(SAXException.class, () -> builder.parse(AttackTestSupport.inputSource(entityPayload(UNLISTED))));
+ }
+
+ @Test
+ @Tag("sax")
+ void saxReaderResolvesAllowListed() throws Exception {
+ final XMLReader reader = hardenedReader();
+ reader.setEntityResolver(ENTITY_ALLOW_LIST);
+ final StringBuilder text = new StringBuilder();
+ reader.setContentHandler(new DefaultHandler() {
+ @Override
+ public void characters(final char[] ch, final int start, final int length) {
+ text.append(ch, start, length);
+ }
+ });
+ reader.parse(AttackTestSupport.inputSource(entityPayload(ALLOWED)));
+ assertTrue(text.toString().contains(AttackTestSupport.LEAKED_MARKER),
+ "allow-listed external entity should resolve through the caller's resolver");
+ }
+
+ @Test
+ @Tag("sax")
+ void saxReaderDeniesUnlisted() throws Exception {
+ final XMLReader reader = hardenedReader();
+ reader.setEntityResolver(ENTITY_ALLOW_LIST);
+ reader.setContentHandler(new DefaultHandler());
+ assertThrows(SAXException.class, () -> reader.parse(AttackTestSupport.inputSource(entityPayload(UNLISTED))));
+ }
+
+ @Test
+ @Tag("sax")
+ void saxParseWithHandlerDoesNotBypass() throws Exception {
+ // SAXParser.parse(source, handler) installs the handler as the reader's entity resolver; the handler does not resolve it (returns null), so the
+ // deny-all floor must still block the external entity rather than letting the parser fetch it.
+ final SAXParser parser = XmlFactories.newSAXParserFactory().newSAXParser();
+ final StringBuilder text = new StringBuilder();
+ final DefaultHandler handler = new DefaultHandler() {
+ @Override
+ public void characters(final char[] ch, final int start, final int length) {
+ text.append(ch, start, length);
+ }
+ };
+ try {
+ parser.parse(AttackTestSupport.inputSource(entityPayload(ALLOWED)), handler);
+ } catch (final SAXException e) {
+ return; // blocked at parse: acceptable
+ }
+ assertFalse(text.toString().contains(AttackTestSupport.LEAKED_MARKER), "parse(source, handler) leaked the external entity:\n" + text);
+ }
+
+ // ---- Entity channel: relative XInclude href (DOM / SAX) ----------------------------------------------------------------------------------------------
+
+ /**
+ * Allow-all resolver: it denies nothing, resolving whatever {@code systemId} it is handed by opening it as a URL. It nonetheless cannot resolve a bare
+ * relative reference such as {@code referenced.xml}, because a plain {@link EntityResolver} (unlike {@link org.xml.sax.ext.EntityResolver2}) is given no
+ * base URI and the SAX2 contract promises it an already-absolutized {@code systemId}. So the resolution fails not from any deny decision but because the
+ * resolver was never handed the whole URL: it succeeds only if the floor absolutizes the XInclude href against the base before consulting the caller.
+ */
+ private static final EntityResolver RESOLVE_ALL = (publicId, systemId) -> {
+ final InputSource source = new InputSource(new URL(systemId).openStream());
+ source.setSystemId(systemId);
+ return source;
+ };
+
+ @Test
+ @Tag("dom")
+ void domResolvesRelativeXIncludeSibling() throws Exception {
+ final DocumentBuilder builder = xIncludeAwareBuilder();
+ builder.setEntityResolver(RESOLVE_ALL);
+ final Document doc = builder.parse(XINCLUDE_HOST);
+ assertTrue(doc.getDocumentElement().getTextContent().contains(AttackTestSupport.LEAKED_MARKER),
+ "relative XInclude sibling should resolve through the caller's resolver after the floor absolutizes the href");
+ }
+
+ @Test
+ @Tag("sax")
+ void saxResolvesRelativeXIncludeSibling() throws Exception {
+ final XMLReader reader = xIncludeAwareReader();
+ reader.setEntityResolver(RESOLVE_ALL);
+ final StringBuilder text = new StringBuilder();
+ reader.setContentHandler(new DefaultHandler() {
+ @Override
+ public void characters(final char[] ch, final int start, final int length) {
+ text.append(ch, start, length);
+ }
+ });
+ reader.parse(XINCLUDE_HOST);
+ assertTrue(text.toString().contains(AttackTestSupport.LEAKED_MARKER),
+ "relative XInclude sibling should resolve through the caller's resolver after the floor absolutizes the href");
+ }
+
+ /** Absolute URL of the host document whose {@code xi:include} references {@code referenced.xml} by a relative href. */
+ private static final String XINCLUDE_HOST = AttackTestSupport.resourceUrl("with-xinclude.xml").toString();
+
+ private static DocumentBuilder xIncludeAwareBuilder() throws Exception {
+ final DocumentBuilderFactory factory = XmlFactories.newDocumentBuilderFactory();
+ factory.setNamespaceAware(true);
+ AttackTestSupport.assumeDoesNotThrow(() -> factory.setXIncludeAware(true));
+ final DocumentBuilder builder = factory.newDocumentBuilder();
+ builder.setErrorHandler(AttackTestSupport.STRICT_REPORTER);
+ return builder;
+ }
+
+ private static XMLReader xIncludeAwareReader() throws Exception {
+ final SAXParserFactory factory = XmlFactories.newSAXParserFactory();
+ factory.setNamespaceAware(true);
+ AttackTestSupport.assumeDoesNotThrow(() -> factory.setXIncludeAware(true));
+ final XMLReader reader = factory.newSAXParser().getXMLReader();
+ reader.setErrorHandler(AttackTestSupport.STRICT_REPORTER);
+ return reader;
+ }
+
+ // ---- Entity channel (StAX) ---------------------------------------------------------------------------------------------------------------------------
+
+ /** Resolves only {@link #ALLOWED} to its content stream; returns {@code null} for anything else. */
+ private static final XMLResolver STAX_ALLOW_LIST = (publicID, systemID, baseURI, namespace) -> {
+ if (!ALLOWED.equals(systemID)) {
+ return null;
+ }
+ try {
+ return new URL(systemID).openStream();
+ } catch (final java.io.IOException e) {
+ throw new XMLStreamException(e);
+ }
+ };
+
+ private static XMLInputFactory externalEntityStaxFactory() {
+ final XMLInputFactory factory = XmlFactories.newXMLInputFactory();
+ factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true);
+ factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, true);
+ return factory;
+ }
+
+ private static String readStaxText(final XMLInputFactory factory, final String payload) throws XMLStreamException {
+ final StringBuilder text = new StringBuilder();
+ final XMLStreamReader stream = factory.createXMLStreamReader(new StringReader(payload));
+ try {
+ while (stream.hasNext()) {
+ final int event = stream.next();
+ if (event == XMLStreamConstants.CHARACTERS || event == XMLStreamConstants.CDATA) {
+ text.append(stream.getText());
+ }
+ }
+ } finally {
+ stream.close();
+ }
+ return text.toString();
+ }
+
+ @Test
+ @Tag("stax")
+ void staxResolvesAllowListed() throws Exception {
+ final XMLInputFactory factory = externalEntityStaxFactory();
+ factory.setXMLResolver(STAX_ALLOW_LIST);
+ assertTrue(readStaxText(factory, entityPayload(ALLOWED)).contains(AttackTestSupport.LEAKED_MARKER),
+ "allow-listed external entity should resolve through the caller's resolver");
+ }
+
+ @Test
+ @Tag("stax")
+ void staxDeniesUnlisted() {
+ final XMLInputFactory factory = externalEntityStaxFactory();
+ factory.setXMLResolver(STAX_ALLOW_LIST);
+ assertThrows(XMLStreamException.class, () -> readStaxText(factory, entityPayload(UNLISTED)));
+ }
+
+ @Test
+ @Tag("stax")
+ void staxCallerCannotRemoveFloor() {
+ // A caller resolver that resolves nothing must not re-open external fetches: the floor still denies.
+ final XMLInputFactory factory = externalEntityStaxFactory();
+ factory.setXMLResolver((publicID, systemID, baseURI, namespace) -> null);
+ assertThrows(XMLStreamException.class, () -> readStaxText(factory, entityPayload(ALLOWED)));
+ }
+
+ @Test
+ @Tag("stax")
+ void staxGetXMLResolverReportsCallerUnwrapped() {
+ final XMLInputFactory factory = XmlFactories.newXMLInputFactory();
+ final XMLResolver caller = (publicID, systemID, baseURI, namespace) -> null;
+ factory.setXMLResolver(caller);
+ assertSame(caller, factory.getXMLResolver(), "getXMLResolver should report the caller's resolver, not the floor wrapper");
+ }
+
+ // ---- Schema channel (LSResourceResolver) -------------------------------------------------------------------------------------------------------------
+
+ /** Absolute location of the imported schema the allow-list resolver permits. */
+ private static final String ALLOWED_SCHEMA = AttackTestSupport.resourceUrl("included.xsd").toString();
+
+ /** Resolves only the {@code included.xsd} import; returns {@code null} for anything else. */
+ private static final LSResourceResolver SCHEMA_ALLOW_LIST = (type, namespaceURI, publicId, systemId, baseURI) ->
+ systemId != null && systemId.endsWith("included.xsd") ? lsInput(ALLOWED_SCHEMA) : null;
+
+ private static LSInput lsInput(final String systemId) {
+ try {
+ final DOMImplementationLS ls = (DOMImplementationLS) DOMImplementationRegistry.newInstance().getDOMImplementation("LS");
+ final LSInput input = ls.createLSInput();
+ input.setByteStream(new URL(systemId).openStream());
+ input.setSystemId(systemId);
+ return input;
+ } catch (final Exception e) {
+ throw new IllegalStateException("Failed to build LSInput for " + systemId, e);
+ }
+ }
+
+ @Test
+ @Tag("schema")
+ void schemaResolvesAllowListed() {
+ // with-import.xsd references an element defined only in the imported included.xsd, so it compiles only if the import is resolved.
+ assertParseSucceeds(() -> {
+ final SchemaFactory factory = XmlFactories.newSchemaFactory();
+ factory.setResourceResolver(SCHEMA_ALLOW_LIST);
+ factory.newSchema(AttackTestSupport.resourceSource("with-import.xsd"));
+ }, "Schema import via caller resolver");
+ }
+
+ @Test
+ @Tag("schema")
+ void schemaDeniesUnlisted() {
+ assertParseFails(() -> {
+ final SchemaFactory factory = XmlFactories.newSchemaFactory();
+ factory.setResourceResolver((type, namespaceURI, publicId, systemId, baseURI) -> null);
+ factory.newSchema(AttackTestSupport.resourceSource("with-import.xsd"));
+ }, "Schema import", SAXException.class, SecurityException.class);
+ }
+
+ // ---- XSLT channel (URIResolver) ----------------------------------------------------------------------------------------------------------------------
+
+ /** Resolves only the {@code included.xsl} import; returns {@code null} for anything else. */
+ private static final URIResolver XSL_ALLOW_LIST = (href, base) ->
+ href != null && href.endsWith("included.xsl") ? AttackTestSupport.resourceSource("included.xsl") : null;
+
+ @Test
+ @Tag("trax")
+ void transformerResolvesAllowListed() {
+ // with-import.xsl imports included.xsl, so it compiles only if the import is resolved.
+ final TransformerFactory factory = hardenedTransformerFactory();
+ factory.setURIResolver(XSL_ALLOW_LIST);
+ assertParseSucceeds(() -> factory.newTemplates(AttackTestSupport.resourceSource("with-import.xsl")), "Stylesheet import via caller resolver");
+ }
+
+ @Test
+ @Tag("trax")
+ void transformerDeniesUnlisted() {
+ final TransformerFactory factory = hardenedTransformerFactory();
+ factory.setURIResolver((href, base) -> null);
+ assertParseFails(() -> factory.newTemplates(AttackTestSupport.resourceSource("with-import.xsl")), "Stylesheet import", TransformerException.class);
+ }
+
+ /**
+ * A hardened {@link TransformerFactory} with a re-throwing error listener. XSLTC and Xalan enforce the deny through the
+ * {@link Resolvers.FallbackDenyURIResolver} floor; Saxon enforces it through its {@code ALLOWED_PROTOCOLS} restrictor. Either way a caller-set resolver that
+ * returns {@code null} cannot re-open the fetch. The strict listener is required because interpretive Xalan routes a blocked {@code xsl:import} through the
+ * error listener and would otherwise recover and compile instead of throwing (XSLTC and Saxon throw regardless).
+ */
+ private static TransformerFactory hardenedTransformerFactory() {
+ final TransformerFactory factory = XmlFactories.newTransformerFactory();
+ factory.setErrorListener(AttackTestSupport.STRICT_REPORTER);
+ return factory;
+ }
+}
diff --git a/src/test/java/org/apache/commons/xml/SchemaLocationPropertyTest.java b/src/test/java/org/apache/commons/xml/SchemaLocationPropertyTest.java
new file mode 100644
index 0000000..463175f
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SchemaLocationPropertyTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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
+ *
+ * https://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.commons.xml;
+
+import static org.apache.commons.xml.AttackTestSupport.assertParseFails;
+import static org.apache.commons.xml.AttackTestSupport.assertParseSucceeds;
+import static org.apache.commons.xml.AttackTestSupport.inputSource;
+import static org.apache.commons.xml.AttackTestSupport.resourceUrl;
+import static org.apache.commons.xml.AttackTestSupport.strictDocumentBuilder;
+import static org.apache.commons.xml.AttackTestSupport.strictXMLReader;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.ThrowingSupplier;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+
+/**
+ * Checks that a hardened, schema-validating parser does not fetch a schema named only through a Xerces schema-location
+ * property: {@code external-noNamespaceSchemaLocation} (no-namespace schema) and {@code external-schemaLocation}
+ * (namespaced schema).
+ *
+ * The fixtures declare the instance's root element, so a parser that fetches the schema validates the instance
+ * cleanly and one that does not cannot. The permissive controls prove the external schema is reachable in principle, so
+ * the hardened side throwing means the fetch was refused, not merely misconfigured. The stock JDK refuses it through
+ * {@code accessExternalSchema=""}; external Apache Xerces, which ignores that property, refuses it through the deny-all
+ * entity-resolver floor.
+ *
+ * Not every parser supports these schema-validation knobs (Android's KXmlParser and Expat do not), so the whole
+ * configuration runs through {@link #configureOrSkip}: a parser that rejects validation, the schema language, or the
+ * schema-location property skips the test rather than failing it.
+ */
+@Tag("schema")
+class SchemaLocationPropertyTest {
+
+ private static final String SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
+ private static final String SCHEMA_FEATURE = "http://apache.org/xml/features/validation/schema";
+ private static final String EXTERNAL_NO_NS = "http://apache.org/xml/properties/schema/external-noNamespaceSchemaLocation";
+ private static final String EXTERNAL_SCHEMA_LOCATION = "http://apache.org/xml/properties/schema/external-schemaLocation";
+
+ /** Instance whose root, {@code }, is declared by {@code no-namespace.xsd}. */
+ private static final String NO_NS_INSTANCE = "x";
+
+ private static final String LEAKED_NS = "http://example.org/leaked";
+
+ /** Instance whose root, {@code l:leaked}, is declared by {@code included.xsd} in the {@value #LEAKED_NS} namespace. */
+ private static final String NAMESPACED_INSTANCE = "x";
+
+ private static String noNamespaceLocation() {
+ return resourceUrl("no-namespace.xsd").toString();
+ }
+
+ private static String namespacedLocation() {
+ return LEAKED_NS + " " + resourceUrl("included.xsd");
+ }
+
+ /**
+ * Runs the parser setup, skipping the test (rather than failing it) on parsers that do not accept these
+ * schema-validation features/properties, such as Android's KXmlParser and Expat.
+ */
+ private static T configureOrSkip(final ThrowingSupplier setup) {
+ try {
+ return setup.get();
+ } catch (final Throwable t) {
+ return Assumptions.abort("Parser does not support schema validation through these features/properties: " + t);
+ }
+ }
+
+ private static DocumentBuilder hardenedValidatingDom(final String property, final String value) {
+ return configureOrSkip(() -> {
+ final DocumentBuilderFactory factory = XmlFactories.newDocumentBuilderFactory();
+ factory.setNamespaceAware(true);
+ factory.setValidating(true);
+ factory.setAttribute(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI);
+ factory.setAttribute(property, value);
+ return strictDocumentBuilder(factory);
+ });
+ }
+
+ private static DocumentBuilder permissiveValidatingDom(final String property, final String value) {
+ return configureOrSkip(() -> {
+ final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ factory.setValidating(true);
+ factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false);
+ factory.setAttribute(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI);
+ factory.setAttribute(property, value);
+ return strictDocumentBuilder(factory);
+ });
+ }
+
+ private static XMLReader hardenedValidatingSax(final String property, final String value) {
+ return configureOrSkip(() -> {
+ final SAXParserFactory factory = XmlFactories.newSAXParserFactory();
+ factory.setNamespaceAware(true);
+ factory.setValidating(true);
+ final SAXParser parser = factory.newSAXParser();
+ parser.setProperty(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI);
+ parser.setProperty(property, value);
+ return strictXMLReader(parser.getXMLReader());
+ });
+ }
+
+ private static XMLReader permissiveValidatingSax(final String property, final String value) {
+ return configureOrSkip(() -> {
+ final SAXParserFactory factory = SAXParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ factory.setValidating(true);
+ factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false);
+ factory.setFeature(SCHEMA_FEATURE, true);
+ final SAXParser parser = factory.newSAXParser();
+ parser.setProperty(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI);
+ parser.setProperty(property, value);
+ return strictXMLReader(parser.getXMLReader());
+ });
+ }
+
+ @Test
+ void hardenedDomRefusesNoNamespaceSchemaLocation() {
+ final DocumentBuilder builder = hardenedValidatingDom(EXTERNAL_NO_NS, noNamespaceLocation());
+ assertParseFails(() -> builder.parse(inputSource(NO_NS_INSTANCE)), "DOM external-noNamespaceSchemaLocation", SAXException.class);
+ }
+
+ @Test
+ void permissiveDomFetchesNoNamespaceSchemaLocation() {
+ final DocumentBuilder builder = permissiveValidatingDom(EXTERNAL_NO_NS, noNamespaceLocation());
+ assertParseSucceeds(() -> builder.parse(inputSource(NO_NS_INSTANCE)), "DOM external-noNamespaceSchemaLocation (permissive)");
+ }
+
+ @Test
+ void hardenedDomRefusesSchemaLocation() {
+ final DocumentBuilder builder = hardenedValidatingDom(EXTERNAL_SCHEMA_LOCATION, namespacedLocation());
+ assertParseFails(() -> builder.parse(inputSource(NAMESPACED_INSTANCE)), "DOM external-schemaLocation", SAXException.class);
+ }
+
+ @Test
+ void permissiveDomFetchesSchemaLocation() {
+ final DocumentBuilder builder = permissiveValidatingDom(EXTERNAL_SCHEMA_LOCATION, namespacedLocation());
+ assertParseSucceeds(() -> builder.parse(inputSource(NAMESPACED_INSTANCE)), "DOM external-schemaLocation (permissive)");
+ }
+
+ @Test
+ void hardenedSaxRefusesNoNamespaceSchemaLocation() {
+ final XMLReader reader = hardenedValidatingSax(EXTERNAL_NO_NS, noNamespaceLocation());
+ assertParseFails(() -> reader.parse(inputSource(NO_NS_INSTANCE)), "SAX external-noNamespaceSchemaLocation", SAXException.class);
+ }
+
+ @Test
+ void permissiveSaxFetchesNoNamespaceSchemaLocation() {
+ final XMLReader reader = permissiveValidatingSax(EXTERNAL_NO_NS, noNamespaceLocation());
+ assertParseSucceeds(() -> reader.parse(inputSource(NO_NS_INSTANCE)), "SAX external-noNamespaceSchemaLocation (permissive)");
+ }
+
+ @Test
+ void hardenedSaxRefusesSchemaLocation() {
+ final XMLReader reader = hardenedValidatingSax(EXTERNAL_SCHEMA_LOCATION, namespacedLocation());
+ assertParseFails(() -> reader.parse(inputSource(NAMESPACED_INSTANCE)), "SAX external-schemaLocation", SAXException.class);
+ }
+
+ @Test
+ void permissiveSaxFetchesSchemaLocation() {
+ final XMLReader reader = permissiveValidatingSax(EXTERNAL_SCHEMA_LOCATION, namespacedLocation());
+ assertParseSucceeds(() -> reader.parse(inputSource(NAMESPACED_INSTANCE)), "SAX external-schemaLocation (permissive)");
+ }
+}
diff --git a/src/test/resources/leaked/no-namespace.xsd b/src/test/resources/leaked/no-namespace.xsd
new file mode 100644
index 0000000..45868ea
--- /dev/null
+++ b/src/test/resources/leaked/no-namespace.xsd
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/test/resources/leaked/with-xinclude.xml b/src/test/resources/leaked/with-xinclude.xml
new file mode 100644
index 0000000..d02533a
--- /dev/null
+++ b/src/test/resources/leaked/with-xinclude.xml
@@ -0,0 +1,5 @@
+
+
+
+
+