View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.xml.secure;
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertFalse;
22  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
23  import static org.junit.jupiter.api.Assertions.assertNotNull;
24  import static org.junit.jupiter.api.Assertions.assertNull;
25  import static org.junit.jupiter.api.Assertions.assertSame;
26  import static org.junit.jupiter.api.Assertions.assertThrows;
27  import static org.junit.jupiter.api.Assertions.assertTrue;
28  import static org.mockito.Mockito.mock;
29  import static org.mockito.Mockito.when;
30  
31  import java.lang.reflect.Field;
32  
33  import javax.xml.XMLConstants;
34  import javax.xml.parsers.DocumentBuilder;
35  import javax.xml.parsers.DocumentBuilderFactory;
36  import javax.xml.parsers.ParserConfigurationException;
37  
38  import org.junit.jupiter.api.Assumptions;
39  import org.junit.jupiter.api.Tag;
40  import org.junit.jupiter.api.Test;
41  import org.junit.jupiter.api.condition.DisabledInNativeImage;
42  
43  @Tag("dom")
44  class SecureDocumentBuilderFactoryTest {
45  
46      /**
47       * Test JAXP provider that delegates builder creation to a Mockito mock.
48       */
49      public static final class MockDocumentBuilderFactory extends DocumentBuilderFactory {
50  
51          private static DocumentBuilderFactory delegate;
52  
53          @Override
54          public Object getAttribute(final String name) {
55              return null;
56          }
57  
58          @Override
59          public boolean getFeature(final String name) {
60              return false;
61          }
62  
63          @Override
64          public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
65              return delegate.newDocumentBuilder();
66          }
67  
68          @Override
69          public void setAttribute(final String name, final Object value) {
70              // no-op
71          }
72  
73          @Override
74          public void setFeature(final String name, final boolean value) {
75              // no-op
76          }
77      }
78  
79      /**
80       * System property naming the {@link DocumentBuilderFactory} implementation, the JVM's mechanism for reconfiguring the default parser.
81       */
82      private static final String FACTORY_ID = "javax.xml.parsers.DocumentBuilderFactory";
83  
84      /**
85       * Gets the implementation a secure factory delegates to, so the selection tests can observe which parser implementation a lookup picked.
86       *
87       * @param factory a secure factory returned by one of the {@code new*Instance} methods; never {@code null}.
88       * @return The wrapped factory.
89       */
90      private static DocumentBuilderFactory getDelegate(final DocumentBuilderFactory factory) throws ReflectiveOperationException {
91          final Field delegate = factory.getClass().getDeclaredField("delegate");
92          delegate.setAccessible(true);
93          return (DocumentBuilderFactory) delegate.get(factory);
94      }
95  
96      /**
97       * Sets the {@value #FACTORY_ID} system property to select the implementation {@link DocumentBuilderFactory#newInstance()} returns.
98       *
99       * @param factoryClassName The implementation class name to install, or {@code null} to clear the property and restore the platform lookup.
100      * @return The previous property value, {@code null} if it was not set; pass it back here to restore the original lookup.
101      */
102     private static String setFactoryIdProperty(final String factoryClassName) {
103         final String previous = System.getProperty(FACTORY_ID);
104         if (factoryClassName == null) {
105             System.clearProperty(FACTORY_ID);
106         } else {
107             System.setProperty(FACTORY_ID, factoryClassName);
108         }
109         return previous;
110     }
111 
112     @Test
113     void createsSecureBuildersFromEveryStaticEntryPoint() throws Exception {
114         Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES, "the platform DOM is left unwrapped: it does not resolve user-defined entities");
115         assertInstanceOf(SecureDocumentBuilder.class, SecureDocumentBuilderFactory.newInstance().newDocumentBuilder());
116         assertInstanceOf(SecureDocumentBuilder.class, SecureDocumentBuilderFactory.newDefaultInstance().newDocumentBuilder());
117         assertInstanceOf(SecureDocumentBuilder.class, SecureDocumentBuilderFactory.newNSInstance().newDocumentBuilder());
118         assertInstanceOf(SecureDocumentBuilder.class, SecureDocumentBuilderFactory.newDefaultNSInstance().newDocumentBuilder());
119     }
120 
121     @Test
122     void createsBuildersDirectly() {
123         final DocumentBuilder builder = SecureDocumentBuilderFactory.newNSDocumentBuilder();
124         assertTrue(builder.isNamespaceAware());
125         if (AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES) {
126             assertInstanceOf(SecureDocumentBuilder.class, builder);
127         }
128     }
129 
130     @Test
131     // Mockito generates the mock classes and its plugin proxies at run time, which a closed-world native image cannot do.
132     @DisabledInNativeImage
133     void newDocumentBuilderWrapsDeclaredExceptions() throws Exception {
134         Assumptions.assumeFalse(AttackTestSupport.IS_ANDROID, "Skipped on Android: parser selection is pinned to the platform implementation");
135         final String previous = setFactoryIdProperty(MockDocumentBuilderFactory.class.getName());
136         try {
137             final ParserConfigurationException cause = new ParserConfigurationException("test");
138             MockDocumentBuilderFactory.delegate = mock(DocumentBuilderFactory.class);
139             when(MockDocumentBuilderFactory.delegate.newDocumentBuilder()).thenThrow(cause);
140             assertSame(cause, assertThrows(IllegalStateException.class, SecureDocumentBuilderFactory::newNSDocumentBuilder).getCause());
141         } finally {
142             setFactoryIdProperty(previous);
143             MockDocumentBuilderFactory.delegate = null;
144         }
145     }
146 
147     @Test
148     void explicitFactoryClassSelectsThatImplementation() throws Exception {
149         Assumptions.assumeFalse(AttackTestSupport.IS_ANDROID, "Skipped on Android: the platform factory is used unwrapped");
150         final Class<?> discovered = DocumentBuilderFactory.newInstance().getClass();
151         final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newNSInstance(discovered.getName(), null);
152         assertEquals(discovered, getDelegate(factory).getClass());
153         assertTrue(factory.isNamespaceAware());
154     }
155 
156     @Test
157     void forwardsEverySupportedFactoryConfiguration() throws Exception {
158         Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES, "the platform DOM is left unwrapped: it does not resolve user-defined entities");
159         final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newInstance();
160         factory.setCoalescing(true);
161         factory.setExpandEntityReferences(false);
162         factory.setIgnoringComments(true);
163         factory.setIgnoringElementContentWhitespace(true);
164         factory.setNamespaceAware(true);
165         factory.setValidating(false);
166         factory.setXIncludeAware(false);
167         factory.setSchema(null);
168         factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
169         factory.setAttribute(TestConstants.JAXP_SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI);
170         assertEquals(XMLConstants.W3C_XML_SCHEMA_NS_URI, factory.getAttribute(TestConstants.JAXP_SCHEMA_LANGUAGE));
171         assertTrue(factory.isCoalescing());
172         assertFalse(factory.isExpandEntityReferences());
173         assertTrue(factory.isIgnoringComments());
174         assertTrue(factory.isIgnoringElementContentWhitespace());
175         assertTrue(factory.isNamespaceAware());
176         assertFalse(factory.isValidating());
177         assertFalse(factory.isXIncludeAware());
178         assertNull(factory.getSchema());
179         assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
180         assertNotNull(factory.newDocumentBuilder());
181     }
182 
183     @Test
184     void newNSInstanceFollowsParserSelection() throws Exception {
185         Assumptions.assumeFalse(AttackTestSupport.IS_ANDROID, "Skipped on Android: the platform factory is used unwrapped");
186         final Class<?> discovered = DocumentBuilderFactory.newInstance().getClass();
187         // no property: the JDK built-in default, unless an override is requested
188         assertEquals(SecureDocumentBuilderFactory.JDK_DOCUMENT_BUILDER_FACTORY,
189                 getDelegate(SecureDocumentBuilderFactory.newNSInstance(false)).getClass().getName());
190         assertEquals(discovered, getDelegate(SecureDocumentBuilderFactory.newNSInstance(true)).getClass());
191         // the factory id property is the JDK's own default reconfiguration; both selections honor it
192         final String previous = setFactoryIdProperty(discovered.getName());
193         try {
194             assertEquals(discovered, getDelegate(SecureDocumentBuilderFactory.newNSInstance(false)).getClass());
195             assertEquals(discovered, getDelegate(SecureDocumentBuilderFactory.newNSInstance(true)).getClass());
196         } finally {
197             setFactoryIdProperty(previous);
198         }
199     }
200 }