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.assertParseFails;
21  import static org.apache.commons.xml.secure.AttackTestSupport.assertParseSucceeds;
22  import static org.apache.commons.xml.secure.AttackTestSupport.inputSource;
23  import static org.apache.commons.xml.secure.AttackTestSupport.resourceUrl;
24  import static org.apache.commons.xml.secure.AttackTestSupport.strictDocumentBuilder;
25  import static org.apache.commons.xml.secure.AttackTestSupport.strictXMLReader;
26  
27  import javax.xml.XMLConstants;
28  import javax.xml.parsers.DocumentBuilder;
29  import javax.xml.parsers.DocumentBuilderFactory;
30  import javax.xml.parsers.SAXParser;
31  import javax.xml.parsers.SAXParserFactory;
32  
33  import org.junit.jupiter.api.Assumptions;
34  import org.junit.jupiter.api.Tag;
35  import org.junit.jupiter.api.Test;
36  import org.junit.jupiter.api.function.ThrowingSupplier;
37  import org.xml.sax.SAXException;
38  import org.xml.sax.XMLReader;
39  
40  /**
41   * Tests that a secure, schema-validating parser does not fetch a schema named only through a Xerces schema-location
42   * property: {@code external-noNamespaceSchemaLocation} (no-namespace schema) and {@code external-schemaLocation}
43   * (namespaced schema).
44   *
45   * <p>
46   * The fixtures declare the instance's root element, so a parser that fetches the schema validates the instance
47   * cleanly and one that does not cannot. The permissive controls prove the external schema is reachable in principle, so
48   * the secure side throwing means the fetch was refused, not merely misconfigured. The ignore-all entity-resolver floor refuses
49   * it on every implementation; the suite runs with {@code javax.xml.accessExternalSchema=all}, so the JAXP 1.5 check plays no
50   * part in the refusal.
51   * </p>
52   *
53   * <p>
54   * Not every parser supports these schema-validation knobs (Android's KXmlParser and Expat do not), so the whole
55   * configuration runs through {@link #configureOrSkip}: a parser that rejects validation, the schema language, or the
56   * schema-location property skips the test rather than failing it.
57   * </p>
58   */
59  @Tag("schema")
60  class SchemaLocationPropertyTest {
61  
62      private static final String SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
63      private static final String SCHEMA_FEATURE = "http://apache.org/xml/features/validation/schema";
64      private static final String EXTERNAL_NO_NS = "http://apache.org/xml/properties/schema/external-noNamespaceSchemaLocation";
65      private static final String EXTERNAL_SCHEMA_LOCATION = "http://apache.org/xml/properties/schema/external-schemaLocation";
66  
67      /**
68       * Instance whose root, {@code <root>}, is declared by {@code no-namespace.xsd}.
69       */
70      private static final String NO_NS_INSTANCE = "<root>x</root>";
71  
72      private static final String LEAKED_NS = "http://example.org/leaked";
73  
74      /**
75       * Instance whose root, {@code l:leaked}, is declared by {@code included.xsd} in the {@value #LEAKED_NS} namespace.
76       */
77      private static final String NAMESPACED_INSTANCE = "<l:leaked xmlns:l=\"" + LEAKED_NS + "\">x</l:leaked>";
78  
79      /**
80       * Runs the parser setup, skipping the test (rather than failing it) on parsers that do not accept these
81       * schema-validation features/properties, such as Android's KXmlParser and Expat.
82       */
83      private static <T> T configureOrSkip(final ThrowingSupplier<T> setup) {
84          try {
85              return setup.get();
86          } catch (final Throwable t) {
87              return Assumptions.abort("Parser does not support schema validation through these features/properties: " + t);
88          }
89      }
90  
91      private static String namespacedLocation() {
92          return LEAKED_NS + " " + resourceUrl("included.xsd");
93      }
94  
95      private static String noNamespaceLocation() {
96          return resourceUrl("no-namespace.xsd").toString();
97      }
98  
99      private static DocumentBuilder permissiveValidatingDom(final String property, final String value) {
100         return configureOrSkip(() -> {
101             final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
102             factory.setNamespaceAware(true);
103             factory.setValidating(true);
104             factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false);
105             factory.setAttribute(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI);
106             factory.setAttribute(property, value);
107             return strictDocumentBuilder(factory);
108         });
109     }
110 
111     private static XMLReader permissiveValidatingSax(final String property, final String value) {
112         return configureOrSkip(() -> {
113             final SAXParserFactory factory = SAXParserFactory.newInstance();
114             factory.setNamespaceAware(true);
115             factory.setValidating(true);
116             factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false);
117             factory.setFeature(SCHEMA_FEATURE, true);
118             final SAXParser parser = factory.newSAXParser();
119             parser.setProperty(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI);
120             parser.setProperty(property, value);
121             return strictXMLReader(parser.getXMLReader());
122         });
123     }
124 
125     private static DocumentBuilder secureValidatingDom(final String property, final String value) {
126         return configureOrSkip(() -> {
127             final DocumentBuilderFactory factory = SecureDocumentBuilderFactory.newInstance();
128             factory.setNamespaceAware(true);
129             factory.setValidating(true);
130             factory.setAttribute(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI);
131             factory.setAttribute(property, value);
132             return strictDocumentBuilder(factory);
133         });
134     }
135 
136     private static XMLReader secureValidatingSax(final String property, final String value) {
137         return configureOrSkip(() -> {
138             final SAXParserFactory factory = SecureSAXParserFactory.newInstance();
139             factory.setNamespaceAware(true);
140             factory.setValidating(true);
141             final SAXParser parser = factory.newSAXParser();
142             parser.setProperty(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI);
143             parser.setProperty(property, value);
144             return strictXMLReader(parser.getXMLReader());
145         });
146     }
147 
148     @Test
149     void permissiveDomFetchesNoNamespaceSchemaLocation() {
150         final DocumentBuilder builder = permissiveValidatingDom(EXTERNAL_NO_NS, noNamespaceLocation());
151         assertParseSucceeds(() -> builder.parse(inputSource(NO_NS_INSTANCE)), "DOM external-noNamespaceSchemaLocation (permissive)");
152     }
153 
154     @Test
155     void permissiveDomFetchesSchemaLocation() {
156         final DocumentBuilder builder = permissiveValidatingDom(EXTERNAL_SCHEMA_LOCATION, namespacedLocation());
157         assertParseSucceeds(() -> builder.parse(inputSource(NAMESPACED_INSTANCE)), "DOM external-schemaLocation (permissive)");
158     }
159 
160     @Test
161     void permissiveSaxFetchesNoNamespaceSchemaLocation() {
162         final XMLReader reader = permissiveValidatingSax(EXTERNAL_NO_NS, noNamespaceLocation());
163         assertParseSucceeds(() -> reader.parse(inputSource(NO_NS_INSTANCE)), "SAX external-noNamespaceSchemaLocation (permissive)");
164     }
165 
166     @Test
167     void permissiveSaxFetchesSchemaLocation() {
168         final XMLReader reader = permissiveValidatingSax(EXTERNAL_SCHEMA_LOCATION, namespacedLocation());
169         assertParseSucceeds(() -> reader.parse(inputSource(NAMESPACED_INSTANCE)), "SAX external-schemaLocation (permissive)");
170     }
171 
172     @Test
173     void secureDomRefusesNoNamespaceSchemaLocation() {
174         final DocumentBuilder builder = secureValidatingDom(EXTERNAL_NO_NS, noNamespaceLocation());
175         assertParseFails(() -> builder.parse(inputSource(NO_NS_INSTANCE)), "DOM external-noNamespaceSchemaLocation", SAXException.class);
176     }
177 
178     @Test
179     void secureDomRefusesSchemaLocation() {
180         final DocumentBuilder builder = secureValidatingDom(EXTERNAL_SCHEMA_LOCATION, namespacedLocation());
181         assertParseFails(() -> builder.parse(inputSource(NAMESPACED_INSTANCE)), "DOM external-schemaLocation", SAXException.class);
182     }
183 
184     @Test
185     void secureSaxRefusesNoNamespaceSchemaLocation() {
186         final XMLReader reader = secureValidatingSax(EXTERNAL_NO_NS, noNamespaceLocation());
187         assertParseFails(() -> reader.parse(inputSource(NO_NS_INSTANCE)), "SAX external-noNamespaceSchemaLocation", SAXException.class);
188     }
189 
190     @Test
191     void secureSaxRefusesSchemaLocation() {
192         final XMLReader reader = secureValidatingSax(EXTERNAL_SCHEMA_LOCATION, namespacedLocation());
193         assertParseFails(() -> reader.parse(inputSource(NAMESPACED_INSTANCE)), "SAX external-schemaLocation", SAXException.class);
194     }
195 }