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:

    *
      - *
    1. {@link HardeningSchemaFactory} installs a deny-all {@link Resolvers.DenyAll#LS_RESOURCE} on the factory (blocking + *
    2. {@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)}.
    3. - *
    4. {@link HardeningSchema} wraps every Validator/ValidatorHandler the inner Schema produces and re-installs the deny-all resolver on each (blocking + *
    5. {@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}.
    6. *
    7. {@link HardeningValidator} rewrites the Source on every {@link Validator#validate(Source)} call.
    8. *
    * *

    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:

    - * + *
      + *
    1. 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.
    2. + *
    3. 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).
    4. + *
    * - *

    {@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

    - * */ 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 @@ + + + + +