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.assertNull;
24  import static org.junit.jupiter.api.Assumptions.assumeTrue;
25  
26  import javax.xml.XMLConstants;
27  import javax.xml.parsers.SAXParser;
28  import javax.xml.parsers.SAXParserFactory;
29  
30  import org.junit.jupiter.api.Tag;
31  import org.junit.jupiter.api.Test;
32  import org.xml.sax.Attributes;
33  import org.xml.sax.InputSource;
34  import org.xml.sax.XMLReader;
35  import org.xml.sax.helpers.DefaultHandler;
36  
37  /**
38   * Tests that a secure {@link SAXParserFactory} performing JAXP 1.2 XSD validation does not fetch an external schema named by an
39   * {@code xsi:noNamespaceSchemaLocation} hint in the instance document.
40   *
41   * <p>
42   * This is the SAX counterpart of {@link SchemaLocationDomTest}. The instance is an empty {@code <root/>} element; the referenced schema declares a default
43   * {@code leak} attribute carrying {@link AttackTestSupport#LEAKED_MARKER}. A parser that fetches the schema augments the element's attributes with that default
44   * (the permissive control observes it in {@link DefaultHandler#startElement}), while a secure parser resolves the schema reference to empty content instead.
45   * Either the empty schema makes the validating parse fail, or the parse completes but the default attribute is never added to the element; either way, the
46   * marker is never observed.
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 it
51   * skips on parsers without it.
52   * </p>
53   */
54  @Tag("sax")
55  class SchemaLocationSaxTest {
56  
57      /**
58       * Captures the root element's schema-defaulted {@code leak} attribute, the SAX-visible signal that the external schema was fetched.
59       */
60      private static final class LeakCapturingHandler extends DefaultHandler {
61          private String leak;
62  
63          @Override
64          public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) {
65              if ("root".equals(localName) || "root".equals(qName)) {
66                  leak = attributes.getValue("leak");
67              }
68          }
69      }
70  
71      /**
72       * JAXP 1.2 property selecting the schema language used by {@link SAXParserFactory#setValidating(boolean)}.
73       */
74      private static final String SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
75  
76      private static final String INSTANCE = "schema-location-instance.xml";
77  
78      private static SAXParser newValidatingParser(final SAXParserFactory factory) throws Exception {
79          factory.setNamespaceAware(true);
80          factory.setValidating(true);
81          final SAXParser parser = factory.newSAXParser();
82          parser.setProperty(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI);
83          return parser;
84      }
85  
86      private static void parse(final SAXParser parser, final DefaultHandler handler) throws Exception {
87          // Drive the XMLReader directly rather than SAXParser.parse(InputSource, DefaultHandler): the latter calls reader.setEntityResolver(handler), which would
88          // clobber the secure ignore-all resolver that external Xerces relies on to block the schemaLocation fetch. Reuse AttackTestSupport's shared strict
89          // reporter as the error handler so a blocked fetch surfaces as a thrown exception rather than a silent recovery.
90          final XMLReader reader = parser.getXMLReader();
91          reader.setContentHandler(handler);
92          AttackTestSupport.strictXMLReader(reader);
93          reader.parse(new InputSource(resourceUrl(INSTANCE).toString()));
94      }
95  
96      private static boolean supportsSchemaLanguage() {
97          try {
98              final SAXParserFactory factory = SAXParserFactory.newInstance();
99              factory.setNamespaceAware(true);
100             factory.setValidating(true);
101             factory.newSAXParser().setProperty(SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI);
102             return true;
103         } catch (final Exception e) {
104             return false;
105         }
106     }
107 
108     @Test
109     void secureDoesNotFetchExternalSchema() throws Exception {
110         assumeTrue(supportsSchemaLanguage(), "parser does not support JAXP 1.2 schema-language XSD validation");
111         final SAXParser parser = newValidatingParser(SecureSAXParserFactory.newInstance());
112         // The schemaLocation reference resolves to empty rather than being fetched. Either the empty schema fails the validating parse (acceptable), or the
113         // parse completes but the schema's default leak attribute is never augmented onto the element. Either way the marker must not be observed.
114         final LeakCapturingHandler handler = new LeakCapturingHandler();
115         try {
116             parse(parser, handler);
117         } catch (final Exception blocked) {
118             // Acceptable: the empty schema was rejected at parse time, so nothing was fetched or augmented.
119         }
120         assertNull(handler.leak, "Secured parse must not augment the external schema's default attribute onto the element.");
121     }
122 
123     @Test
124     void unconfiguredFetchesExternalSchema() throws Exception {
125         assumeTrue(supportsSchemaLanguage(), "parser does not support JAXP 1.2 schema-language XSD validation");
126         final SAXParserFactory factory = SAXParserFactory.newInstance();
127         factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false);
128         final SAXParser parser = newValidatingParser(factory);
129         // Positive control: without securing the external schema is fetched and its default attribute is augmented onto the root element.
130         final LeakCapturingHandler handler = new LeakCapturingHandler();
131         parse(parser, handler);
132         assertEquals(LEAKED_MARKER, handler.leak,
133                 "Permissive parse should have fetched the external schema and augmented its default attribute onto the element.");
134     }
135 }