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.apache.commons.xml.secure.AttackTestSupport.LEAKED_MARKER;
21  import static org.apache.commons.xml.secure.AttackTestSupport.resourceUrl;
22  import static org.junit.jupiter.api.Assertions.assertEquals;
23  import static org.junit.jupiter.api.Assertions.assertNotEquals;
24  import static org.junit.jupiter.api.Assumptions.assumeTrue;
25  
26  import javax.xml.XMLConstants;
27  import javax.xml.parsers.DocumentBuilder;
28  import javax.xml.parsers.DocumentBuilderFactory;
29  
30  import org.junit.jupiter.api.Tag;
31  import org.junit.jupiter.api.Test;
32  import org.w3c.dom.Document;
33  import org.xml.sax.InputSource;
34  import org.xml.sax.SAXException;
35  import org.xml.sax.SAXParseException;
36  import org.xml.sax.helpers.DefaultHandler;
37  
38  /**
39   * Tests that a secure {@link DocumentBuilderFactory} performing JAXP 1.2 XSD validation does not fetch an external schema named by an
40   * {@code xsi:noNamespaceSchemaLocation} hint in the instance document.
41   *
42   * <p>
43   * The instance is an empty {@code <root/>} element; the referenced schema declares a default {@code leak} attribute carrying
44   * {@link AttackTestSupport#LEAKED_MARKER}. A parser that fetches the schema inlines that default into the DOM (the permissive control), while a secure parser
45   * resolves the schema reference to empty content instead. Either the empty schema makes the validating parse fail, or the parse completes but the default is
46   * never inlined; either way, the marker never reaches the DOM.
47   * </p>
48   *
49   * <p>
50   * The test runs only where the implementation supports JAXP 1.2 schema-language XSD validation (the stock JDK and external Xerces do; Android does not), so
51   * it skips on parsers without it.
52   * </p>
53   */
54  @Tag("dom")
55  class SchemaLocationDomTest {
56  
57      /**
58       * JAXP 1.2 property selecting the schema language used by {@link DocumentBuilderFactory#setValidating(boolean)}.
59       */
60      private static final String SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
61  
62      private static final String INSTANCE = "schema-location-instance.xml";
63  
64      private static DocumentBuilderFactory enableXsdValidation(final DocumentBuilderFactory factory) {
65          factory.setNamespaceAware(true);
66          factory.setValidating(true);
67          factory.setAttribute(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI);
68          return factory;
69      }
70  
71      private static Document parse(final DocumentBuilderFactory factory) throws Exception {
72          final DocumentBuilder builder = factory.newDocumentBuilder();
73          builder.setErrorHandler(new DefaultHandler() {
74              /**
75               * Always throws {@link SAXException}.
76               *
77               * @throws SAXException Thrown on every invocation.
78               */
79              @Override
80              public void error(final SAXParseException exception) throws SAXException {
81                  throw exception;
82              }
83          });
84          return builder.parse(new InputSource(resourceUrl(INSTANCE).toString()));
85      }
86  
87      private static boolean supportsSchemaLanguage() {
88          try {
89              final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
90              factory.setValidating(true);
91              factory.setAttribute(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI);
92              return true;
93          } catch (final Exception e) {
94              return false;
95          }
96      }
97  
98      @Test
99      void secureDoesNotFetchExternalSchema() {
100         assumeTrue(supportsSchemaLanguage(), "parser does not support JAXP 1.2 schema-language XSD validation");
101         final DocumentBuilderFactory factory = enableXsdValidation(SecureDocumentBuilderFactory.newInstance());
102         // The schemaLocation reference resolves to empty rather than being fetched. Either the empty schema fails the validating parse (acceptable), or the
103         // parse completes but the schema's default leak attribute is never inlined. Either way the marker must not reach the DOM.
104         try {
105             final Document document = parse(factory);
106             assertNotEquals(LEAKED_MARKER, document.getDocumentElement().getAttribute("leak"),
107                     "Secured parse must not inline the external schema's default attribute.");
108         } catch (final Exception blocked) {
109             // Acceptable: the empty schema was rejected at parse time, so nothing was fetched or inlined.
110         }
111     }
112 
113     @Test
114     void unconfiguredFetchesExternalSchema() throws Exception {
115         assumeTrue(supportsSchemaLanguage(), "parser does not support JAXP 1.2 schema-language XSD validation");
116         final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
117         factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false);
118         // Positive control: without securing the external schema is fetched and its default attribute is inlined into the DOM.
119         final Document document = parse(enableXsdValidation(factory));
120         assertEquals(LEAKED_MARKER, document.getDocumentElement().getAttribute("leak"),
121                 "Permissive parse should have fetched the external schema and inlined its default attribute.");
122     }
123 }