This is an automated email from the ASF dual-hosted git repository. garydgregory pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/commons-xml.git
commit d06988af9786751a766b3455cc34e7d0f2b8cc0d Author: Gary Gregory <[email protected]> AuthorDate: Sun Aug 30 11:19:10 2026 -0400 Add missing test classes. Coverage went from 75% instructions, 70% branches to 97% instructions, 91% branches. --- src/test/java/com/saxonica/ProviderMarker.java | 25 +++ .../xml/FallbackIgnoreEntityResolver2Test.java | 60 ++++++ .../xml/FallbackIgnoreLSResourceResolverTest.java | 47 +++++ .../commons/xml/FallbackIgnoreURIResolverTest.java | 44 ++++ .../commons/xml/FallbackIgnoreXMLResolverTest.java | 44 ++++ .../org/apache/commons/xml/SaxonProviderTest.java | 120 +++++++++++ .../xml/SecureDocumentBuilderFactoryTest.java | 85 ++++++++ .../commons/xml/SecureDocumentBuilderTest.java | 48 +++++ .../apache/commons/xml/SecureExceptionTest.java | 42 ++++ .../commons/xml/SecureSAXParserFactoryTest.java | 100 +++++++++ .../apache/commons/xml/SecureSAXParserTest.java | 126 ++++++++++++ .../commons/xml/SecureSchemaFactoryTest.java | 118 +++++++++++ .../org/apache/commons/xml/SecureSchemaTest.java | 33 +++ .../commons/xml/SecureTemplatesHandlerTest.java | 151 ++++++++++++++ .../apache/commons/xml/SecureTemplatesTest.java | 60 ++++++ .../commons/xml/SecureTransformerFactoryTest.java | 223 ++++++++++++++++++++ .../commons/xml/SecureTransformerHandlerTest.java | 64 ++++++ .../apache/commons/xml/SecureTransformerTest.java | 71 +++++++ .../apache/commons/xml/SecureValidatorTest.java | 116 +++++++++++ .../apache/commons/xml/SecureXMLFilterTest.java | 226 +++++++++++++++++++++ .../apache/commons/xml/SecureXMLReaderTest.java | 70 +++++++ .../commons/xml/SecureXPathExpressionTest.java | 41 ++++ .../apache/commons/xml/SecureXPathFactoryTest.java | 117 +++++++++++ .../org/apache/commons/xml/SecureXPathTest.java | 141 +++++++++++++ 24 files changed, 2172 insertions(+) diff --git a/src/test/java/com/saxonica/ProviderMarker.java b/src/test/java/com/saxonica/ProviderMarker.java new file mode 100644 index 0000000..328ebcc --- /dev/null +++ b/src/test/java/com/saxonica/ProviderMarker.java @@ -0,0 +1,25 @@ +/* + * 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 com.saxonica; + +/** Test-only marker for SaxonProvider's commercial-package recognition path. */ +public final class ProviderMarker { + + private ProviderMarker() { + } +} diff --git a/src/test/java/org/apache/commons/xml/FallbackIgnoreEntityResolver2Test.java b/src/test/java/org/apache/commons/xml/FallbackIgnoreEntityResolver2Test.java new file mode 100644 index 0000000..a86d03d --- /dev/null +++ b/src/test/java/org/apache/commons/xml/FallbackIgnoreEntityResolver2Test.java @@ -0,0 +1,60 @@ +/* + * 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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.ext.DefaultHandler2; +import org.xml.sax.ext.EntityResolver2; + +class FallbackIgnoreEntityResolver2Test { + + @Test + void resolvesDelegatesAndAllFallbackPaths() throws Exception { + final FallbackIgnoreEntityResolver2 floor = new FallbackIgnoreEntityResolver2(null); + assertEquals("https://example.test/base/entity.dtd", floor.resolveEntity("name", "public", "https://example.test/base/", "entity.dtd").getSystemId()); + assertEquals("entity.dtd", floor.resolveEntity("name", "public", "not a URI", "entity.dtd").getSystemId()); + assertNotNull(floor.resolveEntity("public", null)); + final InputSource expected = new org.xml.sax.InputSource(); + final EntityResolver plain = (publicId, systemId) -> expected; + floor.setDelegate(plain); + assertSame(expected, floor.resolveEntity("name", "public", "https://example.test/base/", "entity.dtd")); + final EntityResolver2 extended = new DefaultHandler2() { + + @Override + public org.xml.sax.InputSource resolveEntity(final String name, final String publicId, final String base, final String system) { + return expected; + } + }; + floor.setDelegate(extended); + assertSame(expected, floor.resolveEntity("name", "public", "base", "system")); + floor.setDelegate(null); + System.setProperty(SecureException.THROW_ON_UNRESOLVED, "true"); + try { + assertThrows(org.xml.sax.SAXException.class, () -> floor.resolveEntity("p", "s")); + } finally { + System.clearProperty(SecureException.THROW_ON_UNRESOLVED); + } + } +} diff --git a/src/test/java/org/apache/commons/xml/FallbackIgnoreLSResourceResolverTest.java b/src/test/java/org/apache/commons/xml/FallbackIgnoreLSResourceResolverTest.java new file mode 100644 index 0000000..9c76314 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/FallbackIgnoreLSResourceResolverTest.java @@ -0,0 +1,47 @@ +/* + * 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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.Test; + +class FallbackIgnoreLSResourceResolverTest { + + @Test + void coversDelegateFallbackAndDenyBranches() { + final FallbackIgnoreLSResourceResolver resolver = new FallbackIgnoreLSResourceResolver(null); + final org.w3c.dom.ls.LSInput fallback = resolver.resolveResource("t", "n", "p", "s", "b"); + assertEquals("p", fallback.getPublicId()); + assertEquals("s", fallback.getSystemId()); + assertEquals("b", fallback.getBaseURI()); + final org.w3c.dom.ls.LSInput expected = fallback; + final org.w3c.dom.ls.LSResourceResolver delegate = (type, namespace, publicId, systemId, base) -> expected; + resolver.setDelegate(delegate); + assertSame(delegate, resolver.getDelegate()); + assertSame(expected, resolver.resolveResource("t", "n", "p", "s", "b")); + resolver.setDelegate(null); + System.setProperty(SecureException.THROW_ON_UNRESOLVED, "true"); + try { + assertThrows(org.w3c.dom.ls.LSException.class, () -> resolver.resolveResource("t", "n", "p", "s", "b")); + } finally { + System.clearProperty(SecureException.THROW_ON_UNRESOLVED); + } + } +} diff --git a/src/test/java/org/apache/commons/xml/FallbackIgnoreURIResolverTest.java b/src/test/java/org/apache/commons/xml/FallbackIgnoreURIResolverTest.java new file mode 100644 index 0000000..2d156d8 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/FallbackIgnoreURIResolverTest.java @@ -0,0 +1,44 @@ +/* + * 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.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.Test; + +class FallbackIgnoreURIResolverTest { + + @Test + void resolvesDelegatedAndFallbackSources() throws Exception { + final javax.xml.transform.dom.DOMSource empty = new javax.xml.transform.dom.DOMSource(); + final FallbackIgnoreURIResolver resolver = new FallbackIgnoreURIResolver(null, () -> empty, () -> false); + assertSame(empty, resolver.resolve("href", "base")); + final javax.xml.transform.dom.DOMSource delegated = new javax.xml.transform.dom.DOMSource(); + final javax.xml.transform.URIResolver delegate = (href, base) -> delegated; + resolver.setDelegate(delegate); + assertSame(delegate, resolver.getDelegate()); + assertSame(delegated, resolver.resolve("href", "base")); + resolver.setDelegate(null); + System.setProperty(SecureException.THROW_ON_UNRESOLVED, "true"); + try { + assertThrows(javax.xml.transform.TransformerException.class, () -> resolver.resolve("href", "base")); + } finally { + System.clearProperty(SecureException.THROW_ON_UNRESOLVED); + } + } +} diff --git a/src/test/java/org/apache/commons/xml/FallbackIgnoreXMLResolverTest.java b/src/test/java/org/apache/commons/xml/FallbackIgnoreXMLResolverTest.java new file mode 100644 index 0000000..4c0504d --- /dev/null +++ b/src/test/java/org/apache/commons/xml/FallbackIgnoreXMLResolverTest.java @@ -0,0 +1,44 @@ +/* + * 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.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.Test; + +class FallbackIgnoreXMLResolverTest { + + @Test + void coversDelegateFallbackAndDenyBranches() throws Exception { + final FallbackIgnoreXMLResolver resolver = new FallbackIgnoreXMLResolver(null); + assertNotNull(resolver.resolveEntity("p", "s", "b", "n")); + final Object expected = new Object(); + final javax.xml.stream.XMLResolver delegate = (publicId, systemId, base, namespace) -> expected; + resolver.setDelegate(delegate); + assertSame(delegate, resolver.getDelegate()); + assertSame(expected, resolver.resolveEntity("p", "s", "b", "n")); + resolver.setDelegate(null); + System.setProperty(SecureException.THROW_ON_UNRESOLVED, "true"); + try { + assertThrows(javax.xml.stream.XMLStreamException.class, () -> resolver.resolveEntity("p", "s", "b", "n")); + } finally { + System.clearProperty(SecureException.THROW_ON_UNRESOLVED); + } + } +} diff --git a/src/test/java/org/apache/commons/xml/SaxonProviderTest.java b/src/test/java/org/apache/commons/xml/SaxonProviderTest.java new file mode 100644 index 0000000..7c1abf8 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SaxonProviderTest.java @@ -0,0 +1,120 @@ +/* + * 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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.xml.transform.TransformerFactory; +import javax.xml.xpath.XPathFactory; +import org.junit.jupiter.api.Test; + +class SaxonProviderTest { + + /** Reader used to force SecureConfiguration.makeParser through its SecureException translation path. */ + public static final class FailingXMLReader extends org.xml.sax.helpers.XMLFilterImpl { + + @Override + public void setFeature(final String name, final boolean value) throws org.xml.sax.SAXNotSupportedException { + throw new org.xml.sax.SAXNotSupportedException(name); + } + } + + private static Class<?> loadSaxon(final String className) { + try { + return Class.forName(className); + } catch (final ClassNotFoundException e) { + throw new AssertionError(e); + } + } + + private static Object newSaxon(final String className) throws ReflectiveOperationException { + return loadSaxon(className).getConstructor().newInstance(); + } + + @Test + @org.junit.jupiter.api.Tag("xpath3") + void configuresSaxonFactoriesAndSuppliesAnEmptySource() throws ReflectiveOperationException { + final TransformerFactory transformerFactory = TransformerFactory.class.cast(newSaxon("net.sf.saxon.TransformerFactoryImpl")); + final XPathFactory xpathFactory = XPathFactory.class.cast(newSaxon("net.sf.saxon.xpath.XPathFactoryImpl")); + assertSame(transformerFactory, SaxonProvider.configure(transformerFactory)); + assertSame(xpathFactory, SaxonProvider.configure(xpathFactory)); + assertEquals("net.sf.saxon.lib.EmptySource", SaxonProvider.emptySourceSupplier().get().getClass().getName()); + } + + @Test + void recognizesNonSaxonClass() { + assertFalse(SaxonProvider.isSaxon(getClass())); + } + + @Test + @org.junit.jupiter.api.Tag("xpath3") + void recognizesOpenSourceAndCommercialSaxonClasses() { + assertTrue(SaxonProvider.isSaxon(loadSaxon("net.sf.saxon.TransformerFactoryImpl"))); + assertTrue(SaxonProvider.isSaxon(com.saxonica.ProviderMarker.class)); + } + + @Test + @org.junit.jupiter.api.Tag("xpath3") + void rejectsFactoriesThatDoNotImplementSaxonApis() { + org.junit.jupiter.api.Assertions.assertThrows(SecureException.class, + () -> SaxonProvider.configure(TransformerFactory.newInstance("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl", null))); + org.junit.jupiter.api.Assertions.assertThrows(SecureException.class, () -> SaxonProvider + .configure(XPathFactory.newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI, "com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl", null))); + } + + @Test + @org.junit.jupiter.api.Tag("xpath3") + void rejectsSaxonCollectionResolutionWhenConfiguredToThrow() throws Exception { + final XPathFactory factory = XPathFactory.class.cast(newSaxon("net.sf.saxon.xpath.XPathFactoryImpl")); + SaxonProvider.configure(factory); + final Object configuration = factory.getClass().getMethod("getConfiguration").invoke(factory); + final Object finder = configuration.getClass().getMethod("getCollectionFinder").invoke(configuration); + final java.lang.reflect.Method findCollection = finder.getClass().getMethod("findCollection", loadSaxon("net.sf.saxon.expr.XPathContext"), + String.class); + final String previous = System.getProperty(SecureException.THROW_ON_UNRESOLVED); + try { + System.setProperty(SecureException.THROW_ON_UNRESOLVED, "true"); + final java.lang.reflect.InvocationTargetException exception = org.junit.jupiter.api.Assertions + .assertThrows(java.lang.reflect.InvocationTargetException.class, () -> findCollection.invoke(finder, null, "urn:collection")); + assertEquals("net.sf.saxon.trans.XPathException", exception.getCause().getClass().getName()); + } finally { + if (previous == null) { + System.clearProperty(SecureException.THROW_ON_UNRESOLVED); + } else { + System.setProperty(SecureException.THROW_ON_UNRESOLVED, previous); + } + } + } + + @Test + @org.junit.jupiter.api.Tag("xpath3") + void translatesSecureParserFailuresToSaxonConfigurationErrors() throws Exception { + final TransformerFactory factory = TransformerFactory.class.cast(newSaxon("net.sf.saxon.TransformerFactoryImpl")); + SaxonProvider.configure(factory); + final Object configuration = factory.getClass().getMethod("getConfiguration").invoke(factory); + final java.lang.reflect.Method makeParser = configuration.getClass().getMethod("makeParser", String.class); + final java.lang.reflect.InvocationTargetException exception = org.junit.jupiter.api.Assertions + .assertThrows(java.lang.reflect.InvocationTargetException.class, () -> makeParser.invoke(configuration, FailingXMLReader.class.getName())); + final javax.xml.transform.TransformerFactoryConfigurationError error = org.junit.jupiter.api.Assertions + .assertInstanceOf(javax.xml.transform.TransformerFactoryConfigurationError.class, exception.getCause()); + org.junit.jupiter.api.Assertions.assertInstanceOf(SecureException.class, error.getException()); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureDocumentBuilderFactoryTest.java b/src/test/java/org/apache/commons/xml/SecureDocumentBuilderFactoryTest.java new file mode 100644 index 0000000..88dace2 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureDocumentBuilderFactoryTest.java @@ -0,0 +1,85 @@ +/* + * 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.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.junit.jupiter.api.Test; + +class SecureDocumentBuilderFactoryTest { + + @Test + void createsSecureBuildersFromEveryStaticEntryPoint() throws Exception { + assertInstanceOf(SecureDocumentBuilder.class, SecureDocumentBuilderFactory.newInstance().newDocumentBuilder()); + assertInstanceOf(SecureDocumentBuilder.class, SecureDocumentBuilderFactory.newDefaultInstance().newDocumentBuilder()); + assertInstanceOf(SecureDocumentBuilder.class, SecureDocumentBuilderFactory.newNSInstance().newDocumentBuilder()); + assertInstanceOf(SecureDocumentBuilder.class, SecureDocumentBuilderFactory.newDefaultNSInstance().newDocumentBuilder()); + } + + @Test + void forwardsEverySupportedFactoryConfiguration() throws Exception { + final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newInstance(); + factory.setCoalescing(true); + factory.setExpandEntityReferences(false); + factory.setIgnoringComments(true); + factory.setIgnoringElementContentWhitespace(true); + factory.setNamespaceAware(true); + factory.setValidating(false); + factory.setXIncludeAware(false); + factory.setSchema(null); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + assertTrue(factory.isCoalescing()); + assertFalse(factory.isExpandEntityReferences()); + assertTrue(factory.isIgnoringComments()); + assertTrue(factory.isIgnoringElementContentWhitespace()); + assertTrue(factory.isNamespaceAware()); + assertFalse(factory.isValidating()); + assertFalse(factory.isXIncludeAware()); + assertNull(factory.getSchema()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + assertNotNull(factory.getAttribute(XMLConstants.ACCESS_EXTERNAL_DTD)); + assertNotNull(factory.newDocumentBuilder()); + } + + @Test + void honorsExplicitFactoryClassAndDefaultParserOverrides() throws Exception { + final String className = DocumentBuilderFactory.newInstance().getClass().getName(); + assertTrue(SecureDocumentBuilderFactory.newNSInstance(className, null).isNamespaceAware()); + assertTrue(SecureDocumentBuilderFactory.newNSInstance(true).isNamespaceAware()); + final String property = "javax.xml.parsers.DocumentBuilderFactory"; + final String previous = System.getProperty(property); + try { + System.setProperty(property, className); + assertTrue(SecureDocumentBuilderFactory.newNSInstance(false).isNamespaceAware()); + } finally { + if (previous == null) { + System.clearProperty(property); + } else { + System.setProperty(property, previous); + } + } + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureDocumentBuilderTest.java b/src/test/java/org/apache/commons/xml/SecureDocumentBuilderTest.java new file mode 100644 index 0000000..1dd599b --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureDocumentBuilderTest.java @@ -0,0 +1,48 @@ +/* + * 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.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import javax.xml.parsers.DocumentBuilderFactory; +import org.junit.jupiter.api.Test; + +class SecureDocumentBuilderTest { + + @Test + void createsDocument() throws Exception { + assertNotNull(new SecureDocumentBuilder(DocumentBuilderFactory.newInstance().newDocumentBuilder()).newDocument()); + } + + @Test + void forwardsDocumentBuilderStateAndDomImplementation() throws Exception { + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setValidating(false); + factory.setXIncludeAware(false); + factory.setSchema(null); + final SecureDocumentBuilder builder = new SecureDocumentBuilder(factory.newDocumentBuilder()); + assertTrue(builder.isNamespaceAware()); + assertFalse(builder.isValidating()); + assertFalse(builder.isXIncludeAware()); + assertNull(builder.getSchema()); + assertNotNull(builder.getDOMImplementation()); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureExceptionTest.java b/src/test/java/org/apache/commons/xml/SecureExceptionTest.java new file mode 100644 index 0000000..56dd97e --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureExceptionTest.java @@ -0,0 +1,42 @@ +/* + * 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.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +class SecureExceptionTest { + + @Test + void formatsFailuresAndReadsUnresolvedProperty() { + final RuntimeException cause = new RuntimeException("cause"); + assertSame(cause, SecureException.featureFailed("feature", this, cause).getCause()); + assertTrue(SecureException.forbidden("type", "namespace", "public", "system", "base").contains("system")); + assertSame(cause, SecureException.readerFailed(cause).getCause()); + System.clearProperty(SecureException.THROW_ON_UNRESOLVED); + assertFalse(SecureException.throwOnUnresolved()); + System.setProperty(SecureException.THROW_ON_UNRESOLVED, "true"); + try { + assertTrue(SecureException.throwOnUnresolved()); + } finally { + System.clearProperty(SecureException.THROW_ON_UNRESOLVED); + } + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureSAXParserFactoryTest.java b/src/test/java/org/apache/commons/xml/SecureSAXParserFactoryTest.java new file mode 100644 index 0000000..7080e6e --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureSAXParserFactoryTest.java @@ -0,0 +1,100 @@ +/* + * 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.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.StringReader; + +import javax.xml.XMLConstants; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; + +import org.junit.jupiter.api.Test; +import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; + +class SecureSAXParserFactoryTest { + + @Test + void createsSecureParsersFromEveryStaticEntryPoint() throws Exception { + assertNotNull(SecureSAXParserFactory.newInstance().newSAXParser()); + assertNotNull(SecureSAXParserFactory.newDefaultInstance().newSAXParser()); + assertNotNull(SecureSAXParserFactory.newNSInstance().newSAXParser()); + assertNotNull(SecureSAXParserFactory.newDefaultNSInstance().newSAXParser()); + } + + @Test + void forwardsFactoryConfigurationAndCreatesNamespaceAwareParsers() throws Exception { + final SAXParserFactory factory = SecureSAXParserFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setValidating(false); + factory.setXIncludeAware(false); + factory.setSchema(null); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + assertTrue(factory.isNamespaceAware()); + assertFalse(factory.isValidating()); + assertFalse(factory.isXIncludeAware()); + assertNull(factory.getSchema()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + assertInstanceOf(SecureSAXParser.class, factory.newSAXParser()); + } + + @Test + void respectsDefaultParserSelectionAndLeavesReadersSecureOnlyOnce() throws Exception { + final String factoryId = "javax.xml.parsers.SAXParserFactory"; + final String previous = System.getProperty(factoryId); + try { + System.setProperty(factoryId, SAXParserFactory.newInstance().getClass().getName()); + assertTrue(SecureSAXParserFactory.newNSInstance(false).isNamespaceAware()); + } finally { + if (previous == null) { + System.clearProperty(factoryId); + } else { + System.setProperty(factoryId, previous); + } + } + assertTrue(SecureSAXParserFactory.newNSInstance(true).isNamespaceAware()); + final XMLReader reader = SecureSAXParserFactory.newXMLReader(false); + assertSame(reader, SecureSAXParserFactory.secure(reader)); + } + + @Test + void securesOnlySourcesThatNeedAReader() throws Exception { + final StreamSource stream = new StreamSource(new StringReader("<root/>")); + final Source securedStream = SecureSAXParserFactory.secure(stream, false); + assertInstanceOf(SAXSource.class, securedStream); + assertInstanceOf(SecureXMLReader.class, ((SAXSource) securedStream).getXMLReader()); + final SAXSource readerless = new SAXSource(new InputSource(new StringReader("<root/>"))); + assertInstanceOf(SAXSource.class, SecureSAXParserFactory.secure(readerless, true)); + final SAXSource empty = new SAXSource(); + assertSame(empty, SecureSAXParserFactory.secure(empty, false)); + final DOMSource dom = new DOMSource(); + assertSame(dom, SecureSAXParserFactory.secure(dom, false)); + final SAXSource suppliedReader = new SAXSource(SecureSAXParserFactory.newXMLReader(false), new InputSource()); + assertSame(suppliedReader, SecureSAXParserFactory.secure(suppliedReader, false)); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureSAXParserTest.java b/src/test/java/org/apache/commons/xml/SecureSAXParserTest.java new file mode 100644 index 0000000..22926a8 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureSAXParserTest.java @@ -0,0 +1,126 @@ +/* + * 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.junit.jupiter.api.Assertions.assertNotNull; +import javax.xml.parsers.SAXParserFactory; +import org.junit.jupiter.api.Test; + +class SecureSAXParserTest { + + private static final class ParserSecureReader extends SecureXMLReader implements org.xml.sax.Parser { + + ParserSecureReader(final org.xml.sax.XMLReader reader) { + super(reader); + } + + @Override + public void setDocumentHandler(final org.xml.sax.DocumentHandler handler) { + } + + @Override + public void setLocale(final java.util.Locale locale) { + } + } + + private static final class ReaderSAXParser extends javax.xml.parsers.SAXParser { + + private final org.xml.sax.XMLReader reader; + + ReaderSAXParser(final org.xml.sax.XMLReader reader) { + this.reader = reader; + } + + @Override + public org.xml.sax.Parser getParser() { + return (org.xml.sax.Parser) reader; + } + + @Override + public Object getProperty(final String name) throws org.xml.sax.SAXNotRecognizedException, org.xml.sax.SAXNotSupportedException { + return reader.getProperty(name); + } + + @Override + public javax.xml.validation.Schema getSchema() { + return null; + } + + @Override + public org.xml.sax.XMLReader getXMLReader() { + return reader; + } + + @Override + public boolean isNamespaceAware() { + return false; + } + + @Override + public boolean isValidating() { + return false; + } + + @Override + public boolean isXIncludeAware() { + return false; + } + + @Override + public void reset() { + } + + @Override + public void setProperty(final String name, final Object value) throws org.xml.sax.SAXNotRecognizedException, org.xml.sax.SAXNotSupportedException { + reader.setProperty(name, value); + } + } + + @Test + void cachesSecureViewsThenRecreatesThemAfterReset() throws Exception { + final SecureSAXParser parser = new SecureSAXParser(SAXParserFactory.newInstance().newSAXParser()); + final org.xml.sax.XMLReader firstReader = parser.getXMLReader(); + final org.xml.sax.Parser firstParser = parser.getParser(); + org.junit.jupiter.api.Assertions.assertSame(firstReader, parser.getXMLReader()); + org.junit.jupiter.api.Assertions.assertSame(firstParser, parser.getParser()); + parser.setProperty("http://xml.org/sax/properties/lexical-handler", null); + org.junit.jupiter.api.Assertions.assertNull(parser.getProperty("http://xml.org/sax/properties/lexical-handler")); + parser.reset(); + org.junit.jupiter.api.Assertions.assertNotSame(firstReader, parser.getXMLReader()); + org.junit.jupiter.api.Assertions.assertNotSame(firstParser, parser.getParser()); + } + + @Test + void exposesSecureParserViewsAndState() throws Exception { + final SecureSAXParser parser = new SecureSAXParser(SAXParserFactory.newInstance().newSAXParser()); + assertNotNull(parser.getXMLReader()); + assertNotNull(parser.getParser()); + parser.getSchema(); + parser.isNamespaceAware(); + parser.isValidating(); + parser.isXIncludeAware(); + parser.reset(); + } + + @Test + void reusesAReaderThatAlreadyImplementsSax1Parser() throws Exception { + final ParserSecureReader reader = new ParserSecureReader(SAXParserFactory.newInstance().newSAXParser().getXMLReader()); + final SecureSAXParser parser = new SecureSAXParser(new ReaderSAXParser(reader)); + org.junit.jupiter.api.Assertions.assertSame(reader, parser.getParser()); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureSchemaFactoryTest.java b/src/test/java/org/apache/commons/xml/SecureSchemaFactoryTest.java new file mode 100644 index 0000000..63cc9c8 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureSchemaFactoryTest.java @@ -0,0 +1,118 @@ +/* + * 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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +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 javax.xml.XMLConstants; +import javax.xml.validation.SchemaFactory; +import org.junit.jupiter.api.Test; +import org.xml.sax.helpers.DefaultHandler; + +class SecureSchemaFactoryTest { + + private static final class PropertySchemaFactory extends SchemaFactory { + + private final SchemaFactory delegate = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + + @Override + public org.xml.sax.ErrorHandler getErrorHandler() { + return delegate.getErrorHandler(); + } + + @Override + public boolean getFeature(final String name) throws org.xml.sax.SAXNotRecognizedException, org.xml.sax.SAXNotSupportedException { + return delegate.getFeature(name); + } + + @Override + public Object getProperty(final String name) { + return "value"; + } + + @Override + public org.w3c.dom.ls.LSResourceResolver getResourceResolver() { + return delegate.getResourceResolver(); + } + + @Override + public boolean isSchemaLanguageSupported(final String language) { + return delegate.isSchemaLanguageSupported(language); + } + + @Override + public javax.xml.validation.Schema newSchema() throws org.xml.sax.SAXException { + return delegate.newSchema(); + } + + @Override + public javax.xml.validation.Schema newSchema(final javax.xml.transform.Source[] sources) throws org.xml.sax.SAXException { + return delegate.newSchema(sources); + } + + @Override + public void setErrorHandler(final org.xml.sax.ErrorHandler handler) { + delegate.setErrorHandler(handler); + } + + @Override + public void setFeature(final String name, final boolean value) throws org.xml.sax.SAXNotRecognizedException, org.xml.sax.SAXNotSupportedException { + delegate.setFeature(name, value); + } + + @Override + public void setProperty(final String name, final Object value) { + } + + @Override + public void setResourceResolver(final org.w3c.dom.ls.LSResourceResolver resolver) { + delegate.setResourceResolver(resolver); + } + } + + @Test + void createsSecureSchemas() throws Exception { + assertInstanceOf(SecureSchema.class, SecureSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema()); + assertInstanceOf(SecureSchema.class, SecureSchemaFactory.newDefaultInstance().newSchema()); + } + + @Test + void forwardsSchemaFactoryConfigurationAndAccessors() throws Exception { + final SchemaFactory factory = SecureSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + final DefaultHandler errorHandler = new DefaultHandler(); + final org.w3c.dom.ls.LSResourceResolver resolver = (type, namespace, publicId, systemId, base) -> null; + factory.setErrorHandler(errorHandler); + factory.setResourceResolver(resolver); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + assertSame(errorHandler, factory.getErrorHandler()); + assertSame(resolver, factory.getResourceResolver()); + assertTrue(factory.isSchemaLanguageSupported(XMLConstants.W3C_XML_SCHEMA_NS_URI)); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + assertThrows(org.xml.sax.SAXNotRecognizedException.class, () -> factory.getProperty("foo")); + } + + @Test + void returnsPropertiesFromTheDelegate() throws Exception { + final SchemaFactory factory = SecureSchemaFactory.secure(new PropertySchemaFactory()); + assertEquals("value", factory.getProperty("test")); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureSchemaTest.java b/src/test/java/org/apache/commons/xml/SecureSchemaTest.java new file mode 100644 index 0000000..69ea9b4 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureSchemaTest.java @@ -0,0 +1,33 @@ +/* + * 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.junit.jupiter.api.Assertions.assertInstanceOf; +import javax.xml.XMLConstants; +import javax.xml.validation.SchemaFactory; +import org.junit.jupiter.api.Test; + +class SecureSchemaTest { + + @Test + void wrapsAllSchemaProducts() throws Exception { + final SecureSchema schema = new SecureSchema(SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(), false); + assertInstanceOf(SecureValidator.class, schema.newValidator()); + assertInstanceOf(SecureValidatorHandler.class, schema.newValidatorHandler()); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureTemplatesHandlerTest.java b/src/test/java/org/apache/commons/xml/SecureTemplatesHandlerTest.java new file mode 100644 index 0000000..9497eb3 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureTemplatesHandlerTest.java @@ -0,0 +1,151 @@ +/* + * 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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.transform.Templates; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.sax.TemplatesHandler; +import javax.xml.transform.stream.StreamSource; + +import org.junit.jupiter.api.Test; +import org.xml.sax.Attributes; +import org.xml.sax.Locator; +import org.xml.sax.helpers.DefaultHandler; + +class SecureTemplatesHandlerTest { + + private static final class RecordingHandler extends DefaultHandler implements TemplatesHandler { + + final List<String> calls = new ArrayList<>(); + + Templates templates; + + String systemId = "initial"; + + @Override + public void characters(final char[] ch, final int start, final int length) { + calls.add("characters:" + start + ':' + length); + } + + @Override + public void endDocument() { + calls.add("endDocument"); + } + + @Override + public void endElement(final String uri, final String localName, final String qName) { + calls.add("endElement:" + uri + ':' + localName + ':' + qName); + } + + @Override + public void endPrefixMapping(final String prefix) { + calls.add("endPrefixMapping:" + prefix); + } + + @Override + public String getSystemId() { + return systemId; + } + + @Override + public Templates getTemplates() { + return templates; + } + + @Override + public void ignorableWhitespace(final char[] ch, final int start, final int length) { + calls.add("ignorableWhitespace:" + start + ':' + length); + } + + @Override + public void processingInstruction(final String target, final String data) { + calls.add("processingInstruction:" + target + ':' + data); + } + + @Override + public void setDocumentLocator(final Locator locator) { + calls.add("setDocumentLocator"); + } + + @Override + public void setSystemId(final String value) { + systemId = value; + calls.add("setSystemId:" + value); + } + + @Override + public void skippedEntity(final String name) { + calls.add("skippedEntity:" + name); + } + + @Override + public void startDocument() { + calls.add("startDocument"); + } + + @Override + public void startElement(final String uri, final String localName, final String qName, final Attributes atts) { + calls.add("startElement:" + uri + ':' + localName + ':' + qName); + } + + @Override + public void startPrefixMapping(final String prefix, final String uri) { + calls.add("startPrefixMapping:" + prefix + ':' + uri); + } + } + + @Test + void forwardsEveryTemplatesHandlerMethodAndWrapsTemplates() throws Exception { + final RecordingHandler delegate = new RecordingHandler(); + delegate.templates = TransformerFactory.newInstance() + .newTemplates(new StreamSource(new StringReader("<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'/>"))); + final SecureTemplatesHandler handler = new SecureTemplatesHandler(delegate, null, null, false); + final char[] chars = { 'x', 'y' }; + handler.characters(chars, 1, 1); + handler.endDocument(); + handler.endElement("u", "l", "q"); + handler.endPrefixMapping("p"); + handler.ignorableWhitespace(chars, 0, 2); + handler.processingInstruction("target", "data"); + handler.setDocumentLocator(null); + handler.setSystemId("system"); + handler.skippedEntity("entity"); + handler.startDocument(); + handler.startElement("u", "l", "q", null); + handler.startPrefixMapping("p", "u"); + assertEquals("system", handler.getSystemId()); + assertInstanceOf(SecureTemplates.class, handler.getTemplates()); + assertEquals(12, delegate.calls.size()); + } + + @Test + void preservesNullTemplates() { + final RecordingHandler delegate = new RecordingHandler(); + assertNull(new SecureTemplatesHandler(delegate, null, null, false).getTemplates()); + assertSame(null, delegate.templates); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureTemplatesTest.java b/src/test/java/org/apache/commons/xml/SecureTemplatesTest.java new file mode 100644 index 0000000..8c94ffb --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureTemplatesTest.java @@ -0,0 +1,60 @@ +/* + * 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.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.StringReader; + +import javax.xml.transform.Templates; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamSource; + +import org.junit.jupiter.api.Test; + +class SecureTemplatesTest { + + @Test + void delegatesPropertiesAndWrapsProducedTransformer() throws Exception { + final Templates delegate = TransformerFactory.newInstance().newTemplates(new StreamSource( + new StringReader("<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'><xsl:template match='/'/></xsl:stylesheet>"))); + final SecureTemplates templates = new SecureTemplates(delegate, (href, base) -> null, null, false); + assertNotNull(templates.getOutputProperties()); + assertInstanceOf(SecureTransformer.class, templates.newTransformer()); + assertNotNull(templates.getDelegate()); + } + + @Test + void preservesANullTransformerFromTheDelegate() throws Exception { + final Templates delegate = new Templates() { + + @Override + public java.util.Properties getOutputProperties() { + return new java.util.Properties(); + } + + @Override + public javax.xml.transform.Transformer newTransformer() { + return null; + } + }; + assertNull(new SecureTemplates(delegate, null, null, false).newTransformer()); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureTransformerFactoryTest.java b/src/test/java/org/apache/commons/xml/SecureTransformerFactoryTest.java new file mode 100644 index 0000000..9663061 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureTransformerFactoryTest.java @@ -0,0 +1,223 @@ +/* + * 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.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.io.StringReader; + +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.TransformerFactory; +import javax.xml.transform.URIResolver; +import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.transform.sax.TemplatesHandler; +import javax.xml.transform.sax.TransformerHandler; +import javax.xml.transform.stream.StreamSource; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.xml.sax.XMLFilter; + +class SecureTransformerFactoryTest { + + private static class NullProductsFactory extends SAXTransformerFactory { + + private final SAXTransformerFactory delegate = (SAXTransformerFactory) TransformerFactory.newInstance(); + + private final java.util.Map<String, Object> attributes = new java.util.HashMap<>(); + + @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 attributes.get(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) { + return null; + } + + @Override + public TemplatesHandler newTemplatesHandler() { + return null; + } + + @Override + public Transformer newTransformer() { + return null; + } + + @Override + public Transformer newTransformer(final Source source) { + return null; + } + + @Override + public TransformerHandler newTransformerHandler() { + return null; + } + + @Override + public TransformerHandler newTransformerHandler(final Source source) { + return null; + } + + @Override + public TransformerHandler newTransformerHandler(final Templates templates) { + return null; + } + + @Override + public XMLFilter newXMLFilter(final Source source) { + return null; + } + + @Override + public XMLFilter newXMLFilter(final Templates templates) { + return null; + } + + @Override + public void setAttribute(final String name, final Object value) { + attributes.put(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); + } + } + + private static final class RejectingFeatureFactory extends NullProductsFactory { + + @Override + public void setFeature(final String name, final boolean value) throws TransformerConfigurationException { + throw new TransformerConfigurationException(name); + } + } + + private static void associatedStylesheet(final SAXTransformerFactory factory, final Source source) throws Exception { + try { + factory.getAssociatedStylesheet(source, null, null, null); + } catch (final TransformerConfigurationException | NullPointerException expected) { + // Saxon signals no matching PI with TransformerConfigurationException; Xalan rejects a source without an InputSource. + } + } + + private static StreamSource stylesheet() { + return new StreamSource(new StringReader( + "<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>" + "<xsl:template match='/'/></xsl:stylesheet>")); + } + + @Test + void preservesNullResultsFromEveryWrappableProduct() throws Exception { + final SAXTransformerFactory factory = (SAXTransformerFactory) SecureTransformerFactory.secure(new NullProductsFactory()); + final Templates templates = TransformerFactory.newInstance().newTemplates(stylesheet()); + assertNull(factory.newTemplates(stylesheet())); + assertNull(factory.newTemplatesHandler()); + assertNull(factory.newTransformer()); + assertNull(factory.newTransformer(stylesheet())); + assertNull(factory.newTransformerHandler()); + assertNull(factory.newTransformerHandler(stylesheet())); + assertNull(factory.newTransformerHandler(templates)); + assertNull(factory.newXMLFilter(stylesheet())); + factory.setAttribute("test", "value"); + org.junit.jupiter.api.Assertions.assertEquals("value", factory.getAttribute("test")); + factory.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true); + } + + @Test + void rejectsDelegatesThatCannotEnableSecureProcessing() { + org.junit.jupiter.api.Assertions.assertThrows(SecureException.class, () -> SecureTransformerFactory.secure(new RejectingFeatureFactory())); + } + + @Test + @Tag("trax") + void securesAssociatedStylesheetSourcesOfEverySupportedShape() throws Exception { + final SAXTransformerFactory factory = (SAXTransformerFactory) SecureTransformerFactory.newInstance(); + associatedStylesheet(factory, new StreamSource(new StringReader("<root/>"))); + associatedStylesheet(factory, new StreamSource(new StringReader("<root>"))); + associatedStylesheet(factory, new javax.xml.transform.sax.SAXSource(new org.xml.sax.InputSource(new StringReader("<root/>")))); + associatedStylesheet(factory, new javax.xml.transform.sax.SAXSource()); + associatedStylesheet(factory, + new javax.xml.transform.sax.SAXSource(SecureSAXParserFactory.newXMLReader(false), new org.xml.sax.InputSource(new StringReader("<root/>")))); + associatedStylesheet(factory, + new javax.xml.transform.dom.DOMSource(javax.xml.parsers.DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument())); + } + + @Test + void wrapsEveryStandardAndSaxFactoryProduct() throws Exception { + final SAXTransformerFactory factory = (SAXTransformerFactory) SecureTransformerFactory.newInstance(); + final URIResolver resolver = (href, base) -> null; + factory.setURIResolver(resolver); + assertSame(resolver, factory.getURIResolver()); + factory.setErrorListener(factory.getErrorListener()); + factory.setAttribute("indent-number", 2); + org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, () -> factory.getAttribute("indent-number")); + final Templates templates = factory.newTemplates(stylesheet()); + assertInstanceOf(SecureTemplates.class, templates); + assertInstanceOf(SecureTransformer.class, factory.newTransformer()); + assertInstanceOf(SecureTransformer.class, factory.newTransformer(stylesheet())); + assertInstanceOf(SecureTemplatesHandler.class, factory.newTemplatesHandler()); + assertInstanceOf(SecureTransformerHandler.class, factory.newTransformerHandler()); + assertInstanceOf(SecureTransformerHandler.class, factory.newTransformerHandler(stylesheet())); + assertInstanceOf(SecureTransformerHandler.class, factory.newTransformerHandler(templates)); + assertInstanceOf(SecureXMLFilter.class, factory.newXMLFilter(stylesheet())); + assertInstanceOf(SecureXMLFilter.class, factory.newXMLFilter(templates)); + final Templates rawTemplates = TransformerFactory.newInstance().newTemplates(stylesheet()); + assertInstanceOf(SecureXMLFilter.class, factory.newXMLFilter(rawTemplates)); + associatedStylesheet(factory, new StreamSource(new StringReader("<root/>"))); + assertInstanceOf(SecureTransformer.class, SecureTransformerFactory.newInstance().newTransformer()); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureTransformerHandlerTest.java b/src/test/java/org/apache/commons/xml/SecureTransformerHandlerTest.java new file mode 100644 index 0000000..217de4f --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureTransformerHandlerTest.java @@ -0,0 +1,64 @@ +/* + * 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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import java.io.StringWriter; + +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.transform.stream.StreamResult; + +import org.junit.jupiter.api.Test; +import org.xml.sax.helpers.AttributesImpl; + +class SecureTransformerHandlerTest { + + @Test + void forwardsEveryTransformerHandlerMethod() throws Exception { + final SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance(); + final SecureTransformerHandler handler = new SecureTransformerHandler(factory.newTransformerHandler(), null, null, false); + final char[] chars = { 'x' }; + handler.setResult(new StreamResult(new StringWriter())); + handler.setDocumentLocator(new org.xml.sax.helpers.LocatorImpl()); + handler.setSystemId("system"); + handler.startDocument(); + handler.startDTD("root", null, null); + handler.endDTD(); + handler.startPrefixMapping("p", "urn:test"); + handler.startElement("", "root", "root", new AttributesImpl()); + handler.startCDATA(); + handler.characters(chars, 0, 1); + handler.ignorableWhitespace(chars, 0, 1); + handler.comment(chars, 0, 1); + handler.endCDATA(); + handler.processingInstruction("t", "d"); + handler.notationDecl("n", "p", "s"); + handler.unparsedEntityDecl("e", "p", "s", "n"); + handler.startEntity("e"); + handler.endEntity("e"); + handler.skippedEntity("e"); + handler.endElement("", "root", "root"); + handler.endPrefixMapping("p"); + handler.endDocument(); + assertEquals("system", handler.getSystemId()); + assertInstanceOf(SecureTransformer.class, handler.getTransformer()); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureTransformerTest.java b/src/test/java/org/apache/commons/xml/SecureTransformerTest.java new file mode 100644 index 0000000..9f635fd --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureTransformerTest.java @@ -0,0 +1,71 @@ +/* + * 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.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Properties; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.junit.jupiter.api.Test; + +class SecureTransformerTest { + + @Test + void forwardsEveryTransformerMethod() throws Exception { + final TransformerFactory factory = TransformerFactory.newInstance(); + final SecureTransformer transformer = new SecureTransformer(factory + .newTemplates(new StreamSource(new StringReader( + "<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'><xsl:template match='/'/></xsl:stylesheet>"))) + .newTransformer(), null, null, false); + transformer.clearParameters(); + transformer.setParameter("p", "v"); + assertNotNull(transformer.getParameter("p")); + transformer.setOutputProperty(OutputKeys.METHOD, "xml"); + assertNotNull(transformer.getOutputProperty(OutputKeys.METHOD)); + transformer.setOutputProperties(new Properties()); + assertNotNull(transformer.getOutputProperties()); + transformer.setErrorListener(new javax.xml.transform.ErrorListener() { + + @Override + public void error(final javax.xml.transform.TransformerException e) { + } + + @Override + public void fatalError(final javax.xml.transform.TransformerException e) { + } + + @Override + public void warning(final javax.xml.transform.TransformerException e) { + } + }); + assertNotNull(transformer.getErrorListener()); + transformer.setURIResolver((href, base) -> null); + assertNotNull(transformer.getURIResolver()); + transformer.transform(new DOMSource(DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()), new StreamResult(new StringWriter())); + transformer.reset(); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureValidatorTest.java b/src/test/java/org/apache/commons/xml/SecureValidatorTest.java new file mode 100644 index 0000000..a143686 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureValidatorTest.java @@ -0,0 +1,116 @@ +/* + * 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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +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 javax.xml.XMLConstants; +import javax.xml.validation.SchemaFactory; + +import org.junit.jupiter.api.Test; +import org.xml.sax.helpers.DefaultHandler; + +class SecureValidatorTest { + + private static final class PropertyValidator extends javax.xml.validation.Validator { + + private final javax.xml.validation.Validator delegate; + + PropertyValidator() throws org.xml.sax.SAXException { + delegate = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema().newValidator(); + } + + @Override + public org.xml.sax.ErrorHandler getErrorHandler() { + return delegate.getErrorHandler(); + } + + @Override + public boolean getFeature(final String name) throws org.xml.sax.SAXNotRecognizedException, org.xml.sax.SAXNotSupportedException { + return delegate.getFeature(name); + } + + @Override + public Object getProperty(final String name) { + return "value"; + } + + @Override + public org.w3c.dom.ls.LSResourceResolver getResourceResolver() { + return delegate.getResourceResolver(); + } + + @Override + public void reset() { + delegate.reset(); + } + + @Override + public void setErrorHandler(final org.xml.sax.ErrorHandler errorHandler) { + delegate.setErrorHandler(errorHandler); + } + + @Override + public void setFeature(final String name, final boolean value) throws org.xml.sax.SAXNotRecognizedException, org.xml.sax.SAXNotSupportedException { + delegate.setFeature(name, value); + } + + @Override + public void setProperty(final String name, final Object object) { + // This test double only needs to supply a property value. + } + + @Override + public void setResourceResolver(final org.w3c.dom.ls.LSResourceResolver resourceResolver) { + delegate.setResourceResolver(resourceResolver); + } + + @Override + public void validate(final javax.xml.transform.Source source, final javax.xml.transform.Result result) + throws org.xml.sax.SAXException, java.io.IOException { + delegate.validate(source, result); + } + } + + @Test + void getsPropertiesFromTheDelegate() throws Exception { + assertEquals("value", new SecureValidator(new PropertyValidator(), false).getProperty("property")); + } + + @Test + void preservesNonRemovableResolverFloorAndForwardsConfiguration() throws Exception { + final SecureValidator validator = new SecureValidator(SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema().newValidator(), false); + final DefaultHandler errorHandler = new DefaultHandler(); + final org.w3c.dom.ls.LSResourceResolver resolver = (type, namespace, publicId, systemId, base) -> null; + validator.setErrorHandler(errorHandler); + assertSame(errorHandler, validator.getErrorHandler()); + assertNull(validator.getResourceResolver()); + validator.setResourceResolver(resolver); + validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + assertSame(resolver, validator.getResourceResolver()); + assertTrue(validator.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + assertThrows(org.xml.sax.SAXNotRecognizedException.class, () -> validator.getProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA)); + validator.reset(); + assertNull(validator.getResourceResolver()); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureXMLFilterTest.java b/src/test/java/org/apache/commons/xml/SecureXMLFilterTest.java new file mode 100644 index 0000000..15ed8fe --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureXMLFilterTest.java @@ -0,0 +1,226 @@ +/* + * 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.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.io.StringReader; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.xml.transform.Result; +import javax.xml.transform.Templates; +import javax.xml.transform.Transformer; +import javax.xml.transform.URIResolver; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamSource; + +import org.junit.jupiter.api.Test; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +class SecureXMLFilterTest { + + private static SecureXMLFilter filter() throws Exception { + final Templates templates = TransformerFactory.newInstance().newTemplates(new StreamSource(new StringReader( + "<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'><xsl:template match='@*|node()'><xsl:copy><xsl:apply-templates select='@*|node()'/></xsl:copy></xsl:template></xsl:stylesheet>"))); + return new SecureXMLFilter(new SecureTemplates(templates, null, null, false)); + } + + @Test + void propagatesSaxFailuresFromTheTransformationHandler() throws Exception { + final SecureXMLFilter filter = filter(); + filter.setContentHandler(new DefaultHandler() { + + @Override + public void startElement(final String uri, final String localName, final String qName, final org.xml.sax.Attributes attributes) + throws SAXException { + throw new SAXException("handler"); + } + }); + final SAXException exception = assertThrows(SAXException.class, () -> filter.parse(new InputSource(new StringReader("<root/>")))); + org.junit.jupiter.api.Assertions.assertEquals("handler", exception.getMessage()); + } + + @Test + void reportsWarningErrorAndFatalErrorUsingSaxShape() throws Exception { + final SecureXMLFilter filter = filter(); + final AtomicInteger reports = new AtomicInteger(); + filter.setErrorHandler(new DefaultHandler() { + + @Override + public void error(final SAXParseException e) { + reports.incrementAndGet(); + } + + @Override + public void fatalError(final SAXParseException e) { + reports.incrementAndGet(); + } + + @Override + public void warning(final SAXParseException e) { + reports.incrementAndGet(); + } + }); + filter.warning(new TransformerException("warning")); + filter.error(new TransformerException("error", new SAXParseException("cause", null))); + final TransformerException fatal = new TransformerException("fatal"); + assertSame(fatal, assertThrows(TransformerException.class, () -> filter.fatalError(fatal))); + org.junit.jupiter.api.Assertions.assertEquals(3, reports.get()); + } + + @Test + void requiresContentHandlerAndTransformsWhenOneIsSet() throws Exception { + final SecureXMLFilter filter = filter(); + assertThrows(SAXException.class, () -> filter.parse(new InputSource(new StringReader("<root/>")))); + filter.setContentHandler(new DefaultHandler()); + filter.parse(new InputSource(new StringReader("<root/>"))); + } + + @Test + void rethrowsAnIoExceptionFromTheTransformer() throws Exception { + final Transformer delegate = TransformerFactory.newInstance().newTransformer(); + final Templates templates = new Templates() { + + @Override + public java.util.Properties getOutputProperties() { + return delegate.getOutputProperties(); + } + + @Override + public Transformer newTransformer() { + return new Transformer() { + + @Override + public void clearParameters() { + delegate.clearParameters(); + } + + @Override + public javax.xml.transform.ErrorListener getErrorListener() { + return delegate.getErrorListener(); + } + + @Override + public java.util.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 javax.xml.transform.ErrorListener listener) { + delegate.setErrorListener(listener); + } + + @Override + public void setOutputProperties(final java.util.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 javax.xml.transform.Source source, final Result result) throws TransformerException { + throw new TransformerException(new IOException("transform")); + } + }; + } + }; + final SecureXMLFilter filter = new SecureXMLFilter(new SecureTemplates(templates, null, null, false)); + filter.setContentHandler(new DefaultHandler()); + final IOException exception = assertThrows(IOException.class, () -> filter.parse(new InputSource(new StringReader("<root/>")))); + org.junit.jupiter.api.Assertions.assertEquals("transform", exception.getMessage()); + } + + @Test + void sendsLexicalEventsToALexicalContentHandler() throws Exception { + final SecureXMLFilter filter = filter(); + filter.setContentHandler(new org.xml.sax.ext.DefaultHandler2()); + filter.parse(new InputSource(new StringReader("<root><!--comment--><![CDATA[text]]></root>"))); + } + + @Test + void wrapsErrorHandlerFailuresAndUsesTheConfiguredParent() throws Exception { + final SecureXMLFilter filter = filter(); + final SAXException handlerFailure = new SAXException("error handler"); + filter.setErrorHandler(new DefaultHandler() { + + @Override + public void warning(final SAXParseException e) throws SAXException { + throw handlerFailure; + } + }); + final TransformerException warning = assertThrows(TransformerException.class, () -> filter.warning(new TransformerException("warning"))); + assertSame(handlerFailure, warning.getCause()); + filter.setContentHandler(new DefaultHandler()); + filter.setParent(SecureSAXParserFactory.newXMLReader(false)); + filter.parse(new InputSource(new StringReader("<root/>"))); + } + + @Test + void wrapsIoFailuresFromTheParentReader() throws Exception { + final SecureXMLFilter filter = filter(); + filter.setContentHandler(new DefaultHandler()); + filter.setParent(new org.xml.sax.helpers.XMLFilterImpl() { + + @Override + public void parse(final InputSource input) throws IOException { + throw new IOException("parent"); + } + }); + final SAXException exception = assertThrows(SAXException.class, () -> filter.parse(new InputSource(new StringReader("<root/>")))); + org.junit.jupiter.api.Assertions.assertNotNull(exception.getCause()); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureXMLReaderTest.java b/src/test/java/org/apache/commons/xml/SecureXMLReaderTest.java new file mode 100644 index 0000000..6c5554c --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureXMLReaderTest.java @@ -0,0 +1,70 @@ +/* + * 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.StringReader; +import javax.xml.parsers.SAXParserFactory; +import org.junit.jupiter.api.Test; +import org.xml.sax.InputSource; +import org.xml.sax.helpers.DefaultHandler; + +class SecureXMLReaderTest { + + private static final class RecordingReader extends org.xml.sax.helpers.XMLFilterImpl { + + boolean inputSourceParsed; + + boolean systemIdParsed; + + @Override + public void parse(final InputSource input) { + inputSourceParsed = true; + } + + @Override + public void parse(final String systemId) { + systemIdParsed = true; + } + } + + @Test + void forwardsBothParseOverloads() throws Exception { + final RecordingReader delegate = new RecordingReader(); + final SecureXMLReader reader = new SecureXMLReader(delegate); + reader.parse(new InputSource()); + reader.parse("system"); + org.junit.jupiter.api.Assertions.assertTrue(delegate.inputSourceParsed); + org.junit.jupiter.api.Assertions.assertTrue(delegate.systemIdParsed); + } + + @Test + void forwardsReaderConfigurationAndParse() throws Exception { + final SecureXMLReader reader = new SecureXMLReader(SAXParserFactory.newInstance().newSAXParser().getXMLReader()); + final DefaultHandler handler = new DefaultHandler(); + reader.setContentHandler(handler); + reader.setDTDHandler(handler); + reader.setErrorHandler(handler); + reader.setEntityResolver((publicId, systemId) -> null); + reader.getContentHandler(); + reader.getDTDHandler(); + reader.getErrorHandler(); + reader.getEntityResolver(); + reader.parse(new InputSource(new StringReader("<root/>"))); + org.junit.jupiter.api.Assertions.assertThrows(java.io.IOException.class, () -> reader.parse("file:/definitely-not-present-commons-xml-test.xml")); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureXPathExpressionTest.java b/src/test/java/org/apache/commons/xml/SecureXPathExpressionTest.java new file mode 100644 index 0000000..390dae4 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureXPathExpressionTest.java @@ -0,0 +1,41 @@ +/* + * 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.junit.jupiter.api.Assertions.assertEquals; + +import java.io.StringReader; + +import javax.xml.xpath.XPathFactory; + +import org.junit.jupiter.api.Test; +import org.xml.sax.InputSource; + +class SecureXPathExpressionTest { + + @Test + void evaluatesEveryXPathExpressionOverload() throws Exception { + final SecureXPathExpression expression = new SecureXPathExpression(XPathFactory.newInstance().newXPath().compile("/root/text()"), false); + final InputSource source = new InputSource(new StringReader("<root>value</root>")); + assertEquals("value", expression.evaluate(source)); + assertEquals("value", expression.evaluate(new InputSource(new StringReader("<root>value</root>")), javax.xml.xpath.XPathConstants.STRING)); + assertEquals("value", expression.evaluate(org.apache.commons.xml.SecureXPath.parse(new InputSource(new StringReader("<root>value</root>")), false))); + assertEquals("value", + expression.evaluate(SecureXPath.parse(new InputSource(new StringReader("<root>value</root>")), false), javax.xml.xpath.XPathConstants.STRING)); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureXPathFactoryTest.java b/src/test/java/org/apache/commons/xml/SecureXPathFactoryTest.java new file mode 100644 index 0000000..d474c2e --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureXPathFactoryTest.java @@ -0,0 +1,117 @@ +/* + * 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.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import javax.xml.xpath.XPathFactory; + +import org.junit.jupiter.api.Test; + +class SecureXPathFactoryTest { + + @Test + void createsAndConfiguresAFactoryForTheDefaultObjectModel() throws Exception { + final XPathFactory factory = SecureXPathFactory.newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI); + final javax.xml.xpath.XPathFunctionResolver resolver = (name, arity) -> null; + factory.setXPathFunctionResolver(resolver); + final javax.xml.xpath.XPathVariableResolver variableResolver = name -> null; + factory.setXPathVariableResolver(variableResolver); + assertTrue(factory.isObjectModelSupported(XPathFactory.DEFAULT_OBJECT_MODEL_URI)); + final SecureXPath xpath = (SecureXPath) factory.newXPath(); + assertSame(resolver, xpath.getXPathFunctionResolver()); + assertSame(variableResolver, xpath.getXPathVariableResolver()); + } + + @Test + void createsSecureXPathFromStaticEntryPoints() { + assertInstanceOf(SecureXPath.class, SecureXPathFactory.newInstance().newXPath()); + assertInstanceOf(SecureXPath.class, SecureXPathFactory.newDefaultInstance().newXPath()); + } + + @Test + void preservesANullXPathFromTheDelegate() { + final XPathFactory delegate = new XPathFactory() { + + @Override + public boolean getFeature(final String name) { + return false; + } + + @Override + public boolean isObjectModelSupported(final String objectModel) { + return true; + } + + @Override + public javax.xml.xpath.XPath newXPath() { + return null; + } + + @Override + public void setFeature(final String name, final boolean value) { + } + + @Override + public void setXPathFunctionResolver(final javax.xml.xpath.XPathFunctionResolver resolver) { + } + + @Override + public void setXPathVariableResolver(final javax.xml.xpath.XPathVariableResolver resolver) { + } + }; + assertNull(SecureXPathFactory.secure(delegate).newXPath()); + } + + @Test + void wrapsARejectedRequiredFeatureInSecureException() { + final XPathFactory rejectingFactory = new XPathFactory() { + + @Override + public boolean getFeature(final String name) { + return false; + } + + @Override + public boolean isObjectModelSupported(final String objectModel) { + return true; + } + + @Override + public javax.xml.xpath.XPath newXPath() { + return null; + } + + @Override + public void setFeature(final String name, final boolean value) throws javax.xml.xpath.XPathFactoryConfigurationException { + throw new javax.xml.xpath.XPathFactoryConfigurationException(name); + } + + @Override + public void setXPathFunctionResolver(final javax.xml.xpath.XPathFunctionResolver resolver) { + } + + @Override + public void setXPathVariableResolver(final javax.xml.xpath.XPathVariableResolver resolver) { + } + }; + org.junit.jupiter.api.Assertions.assertThrows(SecureException.class, () -> SecureXPathFactory.secure(rejectingFactory)); + } +} diff --git a/src/test/java/org/apache/commons/xml/SecureXPathTest.java b/src/test/java/org/apache/commons/xml/SecureXPathTest.java new file mode 100644 index 0000000..b64dae0 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SecureXPathTest.java @@ -0,0 +1,141 @@ +/* + * 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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.StringReader; +import java.util.Collections; + +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; +import javax.xml.xpath.XPathFactory; + +import org.junit.jupiter.api.Test; +import org.xml.sax.InputSource; + +class SecureXPathTest { + + @Test + void delegatesEveryXPathMethod() throws Exception { + final SecureXPath xpath = new SecureXPath(XPathFactory.newInstance().newXPath(), false); + final NamespaceContext context = new NamespaceContext() { + + @Override + public String getNamespaceURI(final String prefix) { + return XMLConstants.NULL_NS_URI; + } + + @Override + public String getPrefix(final String namespaceUri) { + return null; + } + + @Override + public java.util.Iterator<String> getPrefixes(final String namespaceUri) { + return Collections.<String>emptyList().iterator(); + } + }; + xpath.setNamespaceContext(context); + xpath.setXPathFunctionResolver((name, arity) -> null); + xpath.setXPathVariableResolver(name -> null); + assertNotNull(xpath.getNamespaceContext()); + assertNotNull(xpath.getXPathFunctionResolver()); + assertNotNull(xpath.getXPathVariableResolver()); + assertNotNull(xpath.compile("/root")); + assertEquals("value", xpath.evaluate("/root/text()", new InputSource(new StringReader("<root>value</root>")))); + assertEquals("value", xpath.evaluate("/root/text()", new InputSource(new StringReader("<root>value</root>")), javax.xml.xpath.XPathConstants.STRING)); + assertEquals("value", xpath.evaluate("/root/text()", SecureXPath.parse(new InputSource(new StringReader("<root>value</root>")), false))); + assertEquals("value", xpath.evaluate("/root/text()", SecureXPath.parse(new InputSource(new StringReader("<root>value</root>")), false), + javax.xml.xpath.XPathConstants.STRING)); + xpath.reset(); + } + + @Test + void preservesANullCompiledExpressionFromTheDelegate() throws Exception { + final javax.xml.xpath.XPath delegate = new javax.xml.xpath.XPath() { + + @Override + public javax.xml.xpath.XPathExpression compile(final String expression) { + return null; + } + + @Override + public String evaluate(final String expression, final InputSource source) { + return null; + } + + @Override + public Object evaluate(final String expression, final InputSource source, final javax.xml.namespace.QName returnType) { + return null; + } + + @Override + public String evaluate(final String expression, final Object item) { + return null; + } + + @Override + public Object evaluate(final String expression, final Object item, final javax.xml.namespace.QName returnType) { + return null; + } + + @Override + public NamespaceContext getNamespaceContext() { + return null; + } + + @Override + public javax.xml.xpath.XPathFunctionResolver getXPathFunctionResolver() { + return null; + } + + @Override + public javax.xml.xpath.XPathVariableResolver getXPathVariableResolver() { + return null; + } + + @Override + public void reset() { + } + + @Override + public void setNamespaceContext(final NamespaceContext context) { + } + + @Override + public void setXPathFunctionResolver(final javax.xml.xpath.XPathFunctionResolver resolver) { + } + + @Override + public void setXPathVariableResolver(final javax.xml.xpath.XPathVariableResolver resolver) { + } + }; + assertNull(new SecureXPath(delegate, false).compile("/root")); + } + + @Test + void wrapsParseFailuresAsXPathExpressionExceptions() { + final javax.xml.xpath.XPathExpressionException exception = assertThrows(javax.xml.xpath.XPathExpressionException.class, + () -> SecureXPath.parse(new InputSource(new StringReader("<root>")), false)); + assertNotNull(exception.getCause()); + } +}
