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.assertNotNull;
21  import static org.junit.jupiter.api.Assertions.assertNull;
22  import static org.junit.jupiter.api.Assertions.assertThrows;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  
25  import javax.xml.transform.Source;
26  import javax.xml.transform.TransformerConfigurationException;
27  import javax.xml.transform.TransformerFactory;
28  import javax.xml.transform.sax.SAXSource;
29  import javax.xml.transform.stream.StreamSource;
30  
31  import org.junit.jupiter.api.Assumptions;
32  import org.junit.jupiter.api.Tag;
33  import org.junit.jupiter.api.Test;
34  import org.xml.sax.InputSource;
35  
36  /**
37   * Tests the two untrusted inputs {@code getAssociatedStylesheet} handles: the document it scans, and the href the {@code xml-stylesheet} PI names.
38   *
39   * <p>
40   * The scan parses the prolog, where a {@code DOCTYPE} with an external subset is processed before the root element. Apache Xalan runs it on a reader the
41   * engine provisions itself, ignoring one passed in a {@link SAXSource} (XALANJ-2849), and the JDK's XSLTC did the same before 8u162; the wrapper hands those a
42   * {@code DOMSource} it pre-parsed through a secure {@code DocumentBuilder}, so the external DTD resolves to empty instead of being fetched.
43   * </p>
44   *
45   * <p>
46   * The href is attacker-controlled content, so the wrapper routes it through the same floor as any other content-named reference: unresolved by default,
47   * fetched only where a caller's {@code URIResolver} opts it in. Compiling the returned Source is the one documented use of this method, so returning it live
48   * would be handing back a URI the document chose. Tagged {@code trax}, so it runs on the stock JDK, Apache Xalan, Saxon, and the Android runtime.
49   * </p>
50   */
51  @Tag("trax")
52  class AssociatedStylesheetTest {
53  
54      private static TransformerFactory secureFactory() {
55          final TransformerFactory factory = SecureTransformerFactory.newInstance();
56          factory.setErrorListener(AttackTestSupport.STRICT_REPORTER);
57          return factory;
58      }
59  
60      @Test
61      void secureGetAssociatedStylesheetIgnoresExternalDtd() throws TransformerConfigurationException {
62          // The prolog declares an unreachable external DTD; the secure scan resolves it to empty rather than fetching it, so the lookup completes instead of
63          // throwing. The PI is found, and its href is floored, so what comes back names no URI.
64          final Source associated = secureFactory()
65                  .getAssociatedStylesheet(AttackTestSupport.resourceSource("associated-stylesheet.xml"), null, null, null);
66          assertNotNull(associated, "expected the associated stylesheet PI to be found");
67          assertNull(associated.getSystemId(), "the PI href must not come back as a live URI: " + associated.getSystemId());
68      }
69  
70      @Test
71      void secureGetAssociatedStylesheetIgnoresExternalDtdWithCallerReader() throws Exception {
72          // Same scan through a SAXSource carrying a caller-supplied secure reader. Xalan and Java 8 XSLTC drop that reader, so this shape has to be pre-parsed
73          // like the reader-less one rather than passed through.
74          final SAXSource source = new SAXSource(SecureSAXParserFactory.newInstance().newSAXParser().getXMLReader(),
75                  new InputSource(AttackTestSupport.resourceUrl("associated-stylesheet.xml").toString()));
76          final Source associated = secureFactory().getAssociatedStylesheet(source, null, null, null);
77          assertNotNull(associated, "expected the associated stylesheet PI to be found");
78          assertNull(associated.getSystemId(), "the PI href must not come back as a live URI: " + associated.getSystemId());
79      }
80  
81      @Test
82      void secureGetAssociatedStylesheetOptsInThroughResolver() throws TransformerConfigurationException {
83          // A caller resolver is consulted for the href exactly as for any other reference. What comes back names the opted-in stylesheet rather than nothing,
84          // which is what separates an opt-in from the floored default; the floor still reparses it through a secure reader, so the shape is its own.
85          final StreamSource opted = new StreamSource(AttackTestSupport.resourceUrl("included.xsl").toString());
86          final TransformerFactory factory = secureFactory();
87          factory.setURIResolver((href, base) -> href != null && href.endsWith("included.xsl") ? opted : null);
88          final Source associated = factory
89                  .getAssociatedStylesheet(AttackTestSupport.resourceSource("associated-stylesheet-plain.xml"), null, null, null);
90          assertNotNull(associated, "expected the associated stylesheet PI to be found");
91          assertNotNull(associated.getSystemId(), "an opted-in href must resolve to the caller's stylesheet, not to the empty default");
92          assertTrue(associated.getSystemId().endsWith("included.xsl"), "unexpected associated stylesheet: " + associated.getSystemId());
93      }
94  
95      @Test
96      void secureGetAssociatedStylesheetReturnsStylesheet() throws TransformerConfigurationException {
97          // Positive control: a plain document with no DOCTYPE is scanned end to end and its PI found, with the href floored.
98          final Source associated = secureFactory()
99                  .getAssociatedStylesheet(AttackTestSupport.resourceSource("associated-stylesheet-plain.xml"), null, null, null);
100         assertNotNull(associated, "expected the associated stylesheet PI to be found");
101         assertNull(associated.getSystemId(), "the PI href must not come back as a live URI: " + associated.getSystemId());
102     }
103 
104     @Test
105     void unconfiguredGetAssociatedStylesheetFetchesExternalDtd() {
106         // Leak/discrimination control: the unconfigured engine attempts to fetch the unreachable external DTD and fails. Android's KXmlParser does not fetch
107         // external DTDs, so it has nothing to demonstrate here.
108         Assumptions.assumeFalse(AttackTestSupport.IS_ANDROID, "Android's KXmlParser does not fetch external DTDs");
109         assertThrows(TransformerConfigurationException.class, () -> TransformerFactory.newInstance()
110                 .getAssociatedStylesheet(AttackTestSupport.resourceSource("associated-stylesheet.xml"), null, null, null));
111     }
112 }