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.assertFalse;
21  import static org.junit.jupiter.api.Assertions.assertTrue;
22  
23  import java.io.StringWriter;
24  
25  import javax.xml.transform.TransformerException;
26  import javax.xml.transform.TransformerFactory;
27  import javax.xml.transform.stream.StreamResult;
28  
29  import org.junit.jupiter.api.Assumptions;
30  import org.junit.jupiter.api.Tag;
31  import org.junit.jupiter.api.Test;
32  
33  /**
34   * Tests that Saxon's alternate public {@code TransformerFactory} entry point routes to the same locked-down {@code Configuration} as the registered one.
35   *
36   * <p>
37   * Saxon ships {@code net.sf.saxon.BasicTransformerFactory}, a public subclass of the registered {@code net.sf.saxon.TransformerFactoryImpl}, selectable
38   * through the standard TrAX system property. Recognition by package prefix sends it through {@code SaxonProvider}; a name-based recognition would let it fall
39   * to the generic recipe. The probe uses {@code fn:collection}, which bypasses Saxon's resource-resolution chain and fetches directly: only the empty
40   * {@code CollectionFinder} that {@code SaxonProvider} installs closes it, so the generic recipe (which leaves it open even after wrapping) does not. The
41   * factory is instantiated reflectively and the tests skip when Saxon is not on the classpath, so under the surefire group filters the checks are effective on
42   * the test-saxon and test-saxon-xerces executions.
43   * </p>
44   */
45  @Tag("trax")
46  class SaxonAlternateFactoryTest {
47  
48      private static final String BASIC_FACTORY_CLASS = "net.sf.saxon.BasicTransformerFactory";
49  
50      private static void assumeSaxonPresent() {
51          boolean present;
52          try {
53              Class.forName(BASIC_FACTORY_CLASS);
54              present = true;
55          } catch (final ClassNotFoundException e) {
56              present = false;
57          }
58          Assumptions.assumeTrue(present, "Saxon is not on the classpath");
59      }
60  
61      /**
62       * Instantiates {@code BasicTransformerFactory} reflectively, so this test compiles and loads without Saxon on the classpath.
63       */
64      private static TransformerFactory basicSaxonFactory() {
65          try {
66              return (TransformerFactory) Class.forName(BASIC_FACTORY_CLASS).getDeclaredConstructor().newInstance();
67          } catch (final ReflectiveOperationException e) {
68              throw new AssertionError("Cannot instantiate " + BASIC_FACTORY_CLASS, e);
69          }
70      }
71  
72      /**
73       * A {@code collection()} over the test fixtures whose {@code referenced.xml} carries {@link AttackTestSupport#LEAKED_MARKER}.
74       */
75      private static String collectionStylesheet() {
76          final String collection = AttackTestSupport.resourceUrl("referenced.xml").toString().replaceFirst("referenced\\.xml$", "?select=referenced.xml");
77          return "<?xml version=\"1.0\"?>\n"
78                  + "<xsl:stylesheet version=\"3.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n"
79                  + "  <xsl:template match=\"/\">\n"
80                  + "    <leaked><xsl:value-of select=\"string(collection('" + collection + "')/leaked)\"/></leaked>\n"
81                  + "  </xsl:template>\n"
82                  + "</xsl:stylesheet>\n";
83      }
84  
85      private static String transform(final TransformerFactory factory) throws TransformerException {
86          final StringWriter sink = new StringWriter();
87          factory.newTemplates(AttackTestSupport.streamSource(collectionStylesheet())).newTransformer()
88                  .transform(AttackTestSupport.streamSource("<root/>"), new StreamResult(sink));
89          return sink.toString();
90      }
91  
92      @Test
93      void secureBasicFactoryDoesNotLeakCollection() {
94          assumeSaxonPresent();
95          try {
96              final String result = transform(SecureTransformerFactory.secure(basicSaxonFactory()));
97              assertFalse(result.contains(AttackTestSupport.LEAKED_MARKER), "collection() leaked through the alternate Saxon factory:\n" + result);
98          } catch (final TransformerException blocked) {
99              // Throwing is an acceptable outcome since it also prevents leaking the marker.
100         }
101     }
102 
103     @Test
104     void unconfiguredBasicFactoryLeaksCollection() throws TransformerException {
105         assumeSaxonPresent();
106         // Leak control: the bare alternate factory resolves the collection, which is exactly what routing it through SaxonProvider exists to prevent.
107         final String result = transform(basicSaxonFactory());
108         assertTrue(result.contains(AttackTestSupport.LEAKED_MARKER), "bare alternate Saxon factory was expected to resolve collection(), got: " + result);
109     }
110 }