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.io.StringReader;
32  import java.lang.reflect.Field;
33  
34  import javax.xml.XMLConstants;
35  import javax.xml.parsers.ParserConfigurationException;
36  import javax.xml.parsers.SAXParser;
37  import javax.xml.parsers.SAXParserFactory;
38  import javax.xml.transform.Source;
39  import javax.xml.transform.dom.DOMSource;
40  import javax.xml.transform.sax.SAXSource;
41  import javax.xml.transform.stream.StreamSource;
42  
43  import org.junit.jupiter.api.Assumptions;
44  import org.junit.jupiter.api.Tag;
45  import org.junit.jupiter.api.Test;
46  import org.junit.jupiter.api.condition.DisabledInNativeImage;
47  import org.xml.sax.InputSource;
48  import org.xml.sax.SAXException;
49  import org.xml.sax.XMLReader;
50  import org.xml.sax.helpers.DefaultHandler;
51  
52  @Tag("sax")
53  public class SecureSAXParserFactoryTest {
54  
55      /**
56       * Test JAXP provider that delegates parser creation to a Mockito mock.
57       */
58      public static final class MockSAXParserFactory extends SAXParserFactory {
59  
60          private static SAXParserFactory delegate;
61  
62          @Override
63          public boolean getFeature(final String name) {
64              return false;
65          }
66  
67          @Override
68          public SAXParser newSAXParser() throws ParserConfigurationException, org.xml.sax.SAXException {
69              return delegate.newSAXParser();
70          }
71  
72          @Override
73          public void setFeature(final String name, final boolean value) {
74              // no-op
75          }
76      }
77  
78      /**
79       * System property naming the {@link SAXParserFactory} implementation, the JVM's mechanism for reconfiguring the default parser.
80       */
81      private static final String FACTORY_ID = "javax.xml.parsers.SAXParserFactory";
82  
83      private static final String NAMESPACES_FEATURE = "http://xml.org/sax/features/namespaces";
84  
85      /**
86       * Asserts every {@code newXMLReader} and {@code newNSXMLReader} method on the given delegate throws {@link IllegalStateException} with the given cause.
87       *
88       * @param cause    The checked exception the delegate is stubbed to throw.
89       * @param delegate The stubbed factory to route {@link MockSAXParserFactory} to.
90       */
91      private static void assertNewXmlReaderWraps(final Exception cause, final SAXParserFactory delegate) {
92          MockSAXParserFactory.delegate = delegate;
93          assertSame(cause, assertThrows(IllegalStateException.class, () -> SecureSAXParserFactory.newXMLReader(false)).getCause());
94          assertSame(cause, assertThrows(IllegalStateException.class, () -> SecureSAXParserFactory.newNSXMLReader()).getCause());
95          assertSame(cause, assertThrows(IllegalStateException.class, () -> SecureSAXParserFactory.newNSXMLReader(null)).getCause());
96      }
97  
98      /**
99       * Gets the implementation a secure factory delegates to, so the selection tests can observe which parser implementation a lookup picked.
100      *
101      * @param factory a secure factory returned by one of the {@code new*Instance} methods; never {@code null}.
102      * @return The wrapped factory.
103      */
104     private static SAXParserFactory getDelegate(final SAXParserFactory factory) throws ReflectiveOperationException {
105         final Field delegate = factory.getClass().getDeclaredField("delegate");
106         delegate.setAccessible(true);
107         return (SAXParserFactory) delegate.get(factory);
108     }
109 
110     /**
111      * Sets the {@value #FACTORY_ID} system property to select the implementation {@link SAXParserFactory#newInstance()} returns.
112      *
113      * @param factoryClassName The implementation class name to install, or {@code null} to clear the property and restore the platform lookup.
114      * @return The previous property value, {@code null} if it was not set; pass it back here to restore the original lookup.
115      */
116     private static String setFactoryIdProperty(final String factoryClassName) {
117         final String previous = System.getProperty(FACTORY_ID);
118         if (factoryClassName == null) {
119             System.clearProperty(FACTORY_ID);
120         } else {
121             System.setProperty(FACTORY_ID, factoryClassName);
122         }
123         return previous;
124     }
125 
126     @Test
127     void createsSecureParsersFromEveryStaticEntryPoint() throws Exception {
128         assertNotNull(SecureSAXParserFactory.newInstance().newSAXParser());
129         assertNotNull(SecureSAXParserFactory.newDefaultInstance().newSAXParser());
130         assertNotNull(SecureSAXParserFactory.newNSInstance().newSAXParser());
131         assertNotNull(SecureSAXParserFactory.newDefaultNSInstance().newSAXParser());
132     }
133 
134     @Test
135     void createsParsersAndReadersDirectly() throws Exception {
136         final SAXParser parser = SecureSAXParserFactory.newNSSAXParser();
137         assertInstanceOf(SecureSAXParser.class, parser);
138         assertTrue(parser.isNamespaceAware());
139         final DefaultHandler handler = new DefaultHandler();
140         final XMLReader reader = SecureSAXParserFactory.newNSXMLReader(handler);
141         assertInstanceOf(SecureXMLReader.class, reader);
142         assertSame(handler, reader.getContentHandler());
143         assertTrue(reader.getFeature(NAMESPACES_FEATURE));
144         final XMLReader noHandlerReader = SecureSAXParserFactory.newNSXMLReader();
145         assertInstanceOf(SecureXMLReader.class, noHandlerReader);
146         assertNull(noHandlerReader.getContentHandler());
147         assertTrue(noHandlerReader.getFeature(NAMESPACES_FEATURE));
148         assertNull(SecureSAXParserFactory.newNSXMLReader(null).getContentHandler());
149     }
150 
151     @Test
152     void forwardsFactoryConfigurationAndCreatesNamespaceAwareParsers() throws Exception {
153         final SAXParserFactory factory = SecureSAXParserFactory.newInstance();
154         factory.setNamespaceAware(true);
155         factory.setValidating(false);
156         if (AttackTestSupport.SAX_SUPPORTS_XINCLUDE) {
157             factory.setXIncludeAware(false);
158             assertFalse(factory.isXIncludeAware());
159         }
160         if (AttackTestSupport.SAX_SUPPORTS_SCHEMA) {
161             factory.setSchema(null);
162             assertNull(factory.getSchema());
163         }
164         if (AttackTestSupport.SAX_SUPPORTS_SECURE_PROCESSING) {
165             factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
166             assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
167         }
168         assertTrue(factory.isNamespaceAware());
169         assertFalse(factory.isValidating());
170         assertInstanceOf(SecureSAXParser.class, factory.newSAXParser());
171     }
172 
173     @Test
174     void leavesReadersSecureOnlyOnce() {
175         final XMLReader reader = SecureSAXParserFactory.newXMLReader(false);
176         assertSame(reader, SecureSAXParserFactory.secure(reader));
177     }
178 
179     @Test
180     void newNSInstanceFollowsParserSelection() throws Exception {
181         Assumptions.assumeFalse(AttackTestSupport.IS_ANDROID, "Skipped on Android: parser selection is pinned to the platform implementation");
182         final Class<?> discovered = SAXParserFactory.newInstance().getClass();
183         // no property: the JDK built-in default, unless an override is requested
184         assertEquals(SecureSAXParserFactory.JDK_SAX_PARSER_FACTORY, getDelegate(SecureSAXParserFactory.newNSInstance(false)).getClass().getName());
185         assertEquals(discovered, getDelegate(SecureSAXParserFactory.newNSInstance(true)).getClass());
186         // the factory id property is the JDK's own default reconfiguration; both selections honor it
187         final String previous = setFactoryIdProperty(discovered.getName());
188         try {
189             assertEquals(discovered, getDelegate(SecureSAXParserFactory.newNSInstance(false)).getClass());
190             assertEquals(discovered, getDelegate(SecureSAXParserFactory.newNSInstance(true)).getClass());
191         } finally {
192             setFactoryIdProperty(previous);
193         }
194     }
195 
196     @Test
197     void newXmlReaderFollowsParserSelection() throws Exception {
198         Assumptions.assumeFalse(AttackTestSupport.IS_ANDROID, "Skipped on Android: parser selection is pinned to the platform implementation");
199         final Class<?> discovered = SAXParserFactory.newInstance().newSAXParser().getXMLReader().getClass();
200         assertEquals(discovered, ((SecureXMLReader) SecureSAXParserFactory.newXMLReader(true)).getDelegate().getClass());
201         final Class<?> jdkReader =
202                 SAXParserFactory.newInstance(SecureSAXParserFactory.JDK_SAX_PARSER_FACTORY, null).newSAXParser().getXMLReader().getClass();
203         assertEquals(jdkReader, ((SecureXMLReader) SecureSAXParserFactory.newXMLReader(false)).getDelegate().getClass());
204         final String previous = setFactoryIdProperty(SAXParserFactory.newInstance().getClass().getName());
205         try {
206             assertEquals(discovered, ((SecureXMLReader) SecureSAXParserFactory.newXMLReader(false)).getDelegate().getClass());
207         } finally {
208             setFactoryIdProperty(previous);
209         }
210     }
211 
212     @Test
213     // Mockito generates the mock classes and its plugin proxies at run time — impossible in a closed-world native image,
214     // so the stubbed factories these error paths need cannot be built there.
215     @DisabledInNativeImage
216     void newXmlReaderWrapsDeclaredExceptions() throws Exception {
217         Assumptions.assumeFalse(AttackTestSupport.IS_ANDROID, "Skipped on Android: parser selection is pinned to the platform implementation");
218         final String previous = setFactoryIdProperty(MockSAXParserFactory.class.getName());
219         try {
220             // SAXParserFactory.newSAXParser() declares ParserConfigurationException and SAXException
221             final ParserConfigurationException notConfigurable = new ParserConfigurationException("test");
222             SAXParserFactory factory = mock(SAXParserFactory.class);
223             when(factory.newSAXParser()).thenThrow(notConfigurable);
224             assertNewXmlReaderWraps(notConfigurable, factory);
225             assertSame(notConfigurable, assertThrows(IllegalStateException.class, SecureSAXParserFactory::newNSSAXParser).getCause());
226             final SAXException noParser = new SAXException("test");
227             factory = mock(SAXParserFactory.class);
228             when(factory.newSAXParser()).thenThrow(noParser);
229             assertNewXmlReaderWraps(noParser, factory);
230             assertSame(noParser, assertThrows(IllegalStateException.class, SecureSAXParserFactory::newNSSAXParser).getCause());
231             // SAXParser.getXMLReader() declares SAXException
232             final SAXException noReader = new SAXException("test");
233             factory = mock(SAXParserFactory.class);
234             final SAXParser parser = mock(SAXParser.class);
235             when(factory.newSAXParser()).thenReturn(parser);
236             when(parser.getXMLReader()).thenThrow(noReader);
237             assertNewXmlReaderWraps(noReader, factory);
238         } finally {
239             setFactoryIdProperty(previous);
240             MockSAXParserFactory.delegate = null;
241         }
242     }
243 
244     @Test
245     void securesOnlySourcesThatNeedAReader() {
246         final StreamSource stream = new StreamSource(new StringReader("<root/>"));
247         final Source securedStream = SecureSAXParserFactory.secure(stream, false);
248         assertInstanceOf(SAXSource.class, securedStream);
249         assertInstanceOf(SecureXMLReader.class, ((SAXSource) securedStream).getXMLReader());
250         final SAXSource readerless = new SAXSource(new InputSource(new StringReader("<root/>")));
251         assertInstanceOf(SAXSource.class, SecureSAXParserFactory.secure(readerless, true));
252         final SAXSource empty = new SAXSource();
253         assertSame(empty, SecureSAXParserFactory.secure(empty, false));
254         final DOMSource dom = new DOMSource();
255         assertSame(dom, SecureSAXParserFactory.secure(dom, false));
256         final SAXSource suppliedReader = new SAXSource(SecureSAXParserFactory.newXMLReader(false), new InputSource());
257         assertSame(suppliedReader, SecureSAXParserFactory.secure(suppliedReader, false));
258     }
259 }