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 }