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.assertNotSame;
25  import static org.junit.jupiter.api.Assertions.assertThrows;
26  import static org.junit.jupiter.api.Assertions.assertTrue;
27  
28  import java.io.StringReader;
29  
30  import javax.xml.XMLConstants;
31  import javax.xml.parsers.DocumentBuilderFactory;
32  import javax.xml.parsers.FactoryConfigurationError;
33  import javax.xml.parsers.SAXParserFactory;
34  import javax.xml.stream.XMLInputFactory;
35  import javax.xml.transform.TransformerFactory;
36  import javax.xml.transform.TransformerFactoryConfigurationError;
37  import javax.xml.validation.SchemaFactory;
38  import javax.xml.xpath.XPathFactory;
39  
40  import org.junit.jupiter.api.Assumptions;
41  import org.junit.jupiter.api.Tag;
42  import org.junit.jupiter.api.Test;
43  import org.w3c.dom.Document;
44  import org.xml.sax.InputSource;
45  import org.xml.sax.helpers.DefaultHandler;
46  
47  /**
48   * Public-API smoke tests for {@link org.apache.commons.xml.secure}.
49   * <p>
50   * Attack tests live in the {@code attacks} sub-package; this file only verifies that new factories are returned, that they report safe defaults, and that a
51   * benign document still parses successfully.
52   * </p>
53   */
54  class SecureFactoriesSmokeTest {
55  
56      private static final String BENIGN_XML = "<?xml version=\"1.0\"?>\n<root><child>hello</child></root>\n";
57  
58      @Test
59      @Tag("dom")
60      void benignDocumentParses() throws Exception {
61          final Document doc = SecureDocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new StringReader(BENIGN_XML)));
62          assertNotNull(doc);
63          assertNotNull(doc.getDocumentElement());
64      }
65  
66      // The explicit-class-name tests discover the runtime default implementation through the raw JAXP factory,
67      // so they stay portable across the JAXP implementations of the surefire matrix.
68      @Test
69      @Tag("dom")
70      void explicitClassNameDocumentBuilderFactoryIsSecure() throws Exception {
71          Assumptions.assumeTrue(AttackTestSupport.DOM_SUPPORTS_SECURE_PROCESSING, "platform DOM does not support FEATURE_SECURE_PROCESSING");
72          final Class<?> impl = DocumentBuilderFactory.newInstance().getClass();
73          final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newInstance(impl.getName(), impl.getClassLoader());
74          assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
75      }
76  
77      @Test
78      @Tag("dom")
79      void explicitClassNameNSDocumentBuilderFactoryIsNamespaceAware() throws Exception {
80          final Class<?> impl = DocumentBuilderFactory.newInstance().getClass();
81          final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newNSInstance(impl.getName(), impl.getClassLoader());
82          assertTrue(factory.isNamespaceAware());
83          if (AttackTestSupport.DOM_SUPPORTS_SECURE_PROCESSING) {
84              assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
85          }
86      }
87  
88      @Test
89      @Tag("sax")
90      void explicitClassNameNSSAXParserFactoryIsNamespaceAware() throws Exception {
91          final Class<?> impl = SAXParserFactory.newInstance().getClass();
92          final SAXParserFactory factory = SecureSAXParserFactory.newNSInstance(impl.getName(), impl.getClassLoader());
93          assertTrue(factory.isNamespaceAware());
94          if (AttackTestSupport.SAX_SUPPORTS_SECURE_PROCESSING) {
95              assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
96          }
97      }
98  
99      @Test
100     @Tag("sax")
101     void explicitClassNameSAXParserFactoryIsSecure() throws Exception {
102         Assumptions.assumeTrue(AttackTestSupport.SAX_SUPPORTS_SECURE_PROCESSING, "platform SAX does not support FEATURE_SECURE_PROCESSING");
103         final Class<?> impl = SAXParserFactory.newInstance().getClass();
104         final SAXParserFactory factory = SecureSAXParserFactory.newInstance(impl.getName(), impl.getClassLoader());
105         assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
106     }
107 
108     @Test
109     @Tag("schema")
110     void explicitClassNameSchemaFactoryIsSecure() throws Exception {
111         final Class<?> impl = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).getClass();
112         final SchemaFactory factory = SecureSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI, impl.getName(), impl.getClassLoader());
113         // Schema securing is the resolver floor plus wrapped products; FEATURE_SECURE_PROCESSING stays untouched (the secure sub-parsers carry it).
114         assertInstanceOf(SecureSchema.class, factory.newSchema());
115     }
116 
117     @Test
118     @Tag("trax")
119     void explicitClassNameTransformerFactoryIsSecure() {
120         final Class<?> impl = TransformerFactory.newInstance().getClass();
121         final TransformerFactory factory = SecureTransformerFactory.newInstance(impl.getName(), impl.getClassLoader());
122         assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
123     }
124 
125     @Test
126     @Tag("xpath")
127     void explicitClassNameXPathFactoryIsSecure() throws Exception {
128         final Class<?> impl = XPathFactory.newInstance().getClass();
129         final XPathFactory factory = SecureXPathFactory.newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI, impl.getName(), impl.getClassLoader());
130         assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
131     }
132 
133     @Test
134     @Tag("stax")
135     void factoryIdXMLInputFactoryIsSecure() {
136         final String factoryId = "org.apache.commons.xml.secure.test.staxFactory";
137         // XMLInputFactory.newInstance, not newFactory: Android's StAX API predates newFactory, and this file also compiles against android.jar.
138         System.setProperty(factoryId, XMLInputFactory.newInstance().getClass().getName());
139         try {
140             final XMLInputFactory factory = SecureXMLInputFactory.newFactory(factoryId, getClass().getClassLoader());
141             assertEquals(Boolean.TRUE, factory.getProperty(XMLInputFactory.SUPPORT_DTD));
142         } finally {
143             System.clearProperty(factoryId);
144         }
145     }
146 
147     @Test
148     @Tag("stax")
149     void newDefaultFactoryXMLInputFactoryIsSecure() {
150         final XMLInputFactory factory = SecureXMLInputFactory.newDefaultFactory();
151         assertEquals(Boolean.TRUE, factory.getProperty(XMLInputFactory.SUPPORT_DTD));
152     }
153 
154     // The newDefault* methods resolve the Java 9 JAXP method at runtime and fall back to the JDK's built-in implementation on Java 8. The dom and sax
155     // variants also run on Android, whose JAXP predates newDefaultInstance and carries no JDK-internal fallback: the methods degrade there to the standard
156     // lookup, which Android pins to the platform implementation.
157     @Test
158     @Tag("dom")
159     void newDefaultInstanceDocumentBuilderFactoryIsUsable() throws Exception {
160         final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newDefaultInstance();
161         assertNotNull(factory.newDocumentBuilder().parse(new InputSource(new StringReader(BENIGN_XML))).getDocumentElement());
162         if (AttackTestSupport.DOM_SUPPORTS_SECURE_PROCESSING) {
163             assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
164         }
165     }
166 
167     @Test
168     @Tag("sax")
169     void newDefaultInstanceSAXParserFactoryIsUsable() throws Exception {
170         final SAXParserFactory factory = SecureSAXParserFactory.newDefaultInstance();
171         factory.newSAXParser().parse(new InputSource(new StringReader(BENIGN_XML)), new DefaultHandler());
172         if (AttackTestSupport.SAX_SUPPORTS_SECURE_PROCESSING) {
173             assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
174         }
175     }
176 
177     @Test
178     @Tag("schema")
179     void newDefaultInstanceSchemaFactoryIsSecure() throws Exception {
180         final SchemaFactory factory = SecureSchemaFactory.newDefaultInstance();
181         // Schema securing is the resolver floor plus wrapped products; FEATURE_SECURE_PROCESSING stays untouched (the secure sub-parsers carry it).
182         assertInstanceOf(SecureSchema.class, factory.newSchema());
183     }
184 
185     @Test
186     @Tag("trax")
187     void newDefaultInstanceTransformerFactoryIsSecure() {
188         // TrAX is outside the Android newDefaultInstance degradation: the platform provides neither the method nor the JDK class, so the miss still throws.
189         if (AttackTestSupport.IS_ANDROID) {
190             assertThrows(TransformerFactoryConfigurationError.class, SecureTransformerFactory::newDefaultInstance);
191             return;
192         }
193         final TransformerFactory factory = SecureTransformerFactory.newDefaultInstance();
194         assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
195     }
196 
197     @Test
198     @Tag("xpath")
199     void newDefaultInstanceXPathFactoryIsSecure() throws Exception {
200         final XPathFactory factory = SecureXPathFactory.newDefaultInstance();
201         assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
202     }
203 
204     @Test
205     @Tag("dom")
206     void newDefaultNSInstanceDocumentBuilderFactoryIsNamespaceAware() throws Exception {
207         final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newDefaultNSInstance();
208         assertTrue(factory.isNamespaceAware());
209         if (AttackTestSupport.DOM_SUPPORTS_SECURE_PROCESSING) {
210             assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
211         }
212     }
213 
214     @Test
215     @Tag("sax")
216     void newDefaultNSInstanceSAXParserFactoryIsNamespaceAware() throws Exception {
217         final SAXParserFactory factory = SecureSAXParserFactory.newDefaultNSInstance();
218         assertTrue(factory.isNamespaceAware());
219         if (AttackTestSupport.SAX_SUPPORTS_SECURE_PROCESSING) {
220             assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
221         }
222     }
223 
224     @Test
225     @Tag("dom")
226     void newDocumentBuilderFactoryDisablesXIncludeAndValidation() {
227         final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newInstance();
228         if (AttackTestSupport.DOM_SUPPORTS_XINCLUDE) {
229             assertFalse(factory.isXIncludeAware(), "XInclude must be off by default");
230         }
231         assertFalse(factory.isValidating(), "Validation must be off by default");
232     }
233 
234     @Test
235     @Tag("dom")
236     void newDocumentBuilderFactoryEnablesSecureProcessing() throws Exception {
237         Assumptions.assumeTrue(AttackTestSupport.DOM_SUPPORTS_SECURE_PROCESSING, "platform DOM does not support FEATURE_SECURE_PROCESSING");
238         final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newInstance();
239         assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING), "FEATURE_SECURE_PROCESSING must be on");
240     }
241 
242     @Test
243     @Tag("dom")
244     void newDocumentBuilderFactoryReturnsFreshInstance() {
245         final DocumentBuilderFactory a = SecureDocumentBuilderFactory.newInstance();
246         final DocumentBuilderFactory b = SecureDocumentBuilderFactory.newInstance();
247         assertNotNull(a);
248         assertNotNull(b);
249         assertNotSame(a, b);
250     }
251 
252     @Test
253     @Tag("stax")
254     void newFactoryReturnsFreshInstance() {
255         final XMLInputFactory a = SecureXMLInputFactory.newFactory();
256         final XMLInputFactory b = SecureXMLInputFactory.newFactory();
257         assertNotSame(a, b);
258         assertEquals(Boolean.TRUE, a.getProperty(XMLInputFactory.SUPPORT_DTD));
259     }
260 
261     // The newNSInstance family (Java 13) falls back to enabling namespace awareness on the corresponding newInstance lookup, the behavior the JAXP methods
262     // are specified to have, so the non-default variants work on every platform including Android.
263     @Test
264     @Tag("dom")
265     void newNSInstanceDocumentBuilderFactoryIsNamespaceAware() throws Exception {
266         final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newNSInstance();
267         assertTrue(factory.isNamespaceAware());
268         assertNotNull(factory.newDocumentBuilder().parse(new InputSource(new StringReader(BENIGN_XML))).getDocumentElement());
269         if (AttackTestSupport.DOM_SUPPORTS_SECURE_PROCESSING) {
270             assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
271         }
272     }
273 
274     @Test
275     @Tag("sax")
276     void newNSInstanceSAXParserFactoryIsNamespaceAware() throws Exception {
277         final SAXParserFactory factory = SecureSAXParserFactory.newNSInstance();
278         assertTrue(factory.isNamespaceAware());
279         factory.newSAXParser().parse(new InputSource(new StringReader(BENIGN_XML)), new DefaultHandler());
280         if (AttackTestSupport.SAX_SUPPORTS_SECURE_PROCESSING) {
281             assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
282         }
283     }
284 
285     @Test
286     @Tag("sax")
287     void newSAXParserFactoryReturnsFreshInstance() {
288         final SAXParserFactory a = SecureSAXParserFactory.newInstance();
289         final SAXParserFactory b = SecureSAXParserFactory.newInstance();
290         assertNotSame(a, b);
291         assertFalse(a.isValidating());
292         if (AttackTestSupport.SAX_SUPPORTS_XINCLUDE) {
293             assertFalse(a.isXIncludeAware());
294         }
295     }
296 
297     @Test
298     @Tag("schema")
299     void newSchemaFactoryReturnsFreshInstance() throws Exception {
300         final SchemaFactory a = SecureSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
301         final SchemaFactory b = SecureSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
302         assertNotSame(a, b);
303         // Schema securing is the resolver floor plus wrapped products; FEATURE_SECURE_PROCESSING stays untouched (the secure sub-parsers carry it).
304         assertInstanceOf(SecureSchema.class, a.newSchema());
305     }
306 
307     @Test
308     @Tag("trax")
309     void newTransformerFactoryReturnsFreshInstance() {
310         final TransformerFactory a = SecureTransformerFactory.newInstance();
311         final TransformerFactory b = SecureTransformerFactory.newInstance();
312         assertNotSame(a, b);
313     }
314 
315     @Test
316     @Tag("stax")
317     void newXMLInputFactoryReturnsFreshInstance() {
318         final XMLInputFactory a = SecureXMLInputFactory.newInstance();
319         final XMLInputFactory b = SecureXMLInputFactory.newInstance();
320         assertNotSame(a, b);
321         assertEquals(Boolean.TRUE, a.getProperty(XMLInputFactory.SUPPORT_DTD));
322         assertEquals(Boolean.FALSE, a.getProperty(XMLInputFactory.IS_VALIDATING));
323     }
324 
325     @Test
326     @Tag("xpath")
327     void newXPathFactoryReturnsFreshInstance() throws Exception {
328         final XPathFactory a = SecureXPathFactory.newInstance();
329         final XPathFactory b = SecureXPathFactory.newInstance();
330         assertNotSame(a, b);
331         assertTrue(a.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
332     }
333 
334     /**
335      * The public classes must not extend their JAXP factory type: extending it would inherit the JAXP static factory methods, letting a caller obtain an
336      * unsecured factory through an inherited method such as {@code newInstance(String, ClassLoader)} or {@code newDefaultInstance()}.
337      */
338     @Test
339     @Tag("dom")
340     @Tag("sax")
341     @Tag("stax")
342     @Tag("trax")
343     @Tag("xpath")
344     @Tag("schema")
345     void publicClassesDoNotExtendTheirJaxpFactoryType() {
346         assertFalse(DocumentBuilderFactory.class.isAssignableFrom(SecureDocumentBuilderFactory.class));
347         assertFalse(SAXParserFactory.class.isAssignableFrom(SecureSAXParserFactory.class));
348         assertFalse(SchemaFactory.class.isAssignableFrom(SecureSchemaFactory.class));
349         assertFalse(TransformerFactory.class.isAssignableFrom(SecureTransformerFactory.class));
350         assertFalse(XMLInputFactory.class.isAssignableFrom(SecureXMLInputFactory.class));
351         assertFalse(XPathFactory.class.isAssignableFrom(SecureXPathFactory.class));
352     }
353 
354     @Test
355     @Tag("dom")
356     void unknownFactoryClassNameThrows() {
357         assertThrows(FactoryConfigurationError.class, () -> SecureDocumentBuilderFactory.newInstance("no.such.FactoryClass", null));
358     }
359 }