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.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
22  import static org.junit.jupiter.api.Assertions.assertNull;
23  import static org.junit.jupiter.api.Assertions.assertSame;
24  import static org.junit.jupiter.api.Assertions.assertThrows;
25  import static org.junit.jupiter.api.Assertions.assertTrue;
26  
27  import java.lang.reflect.InvocationTargetException;
28  import java.lang.reflect.Method;
29  
30  import javax.xml.xpath.XPath;
31  import javax.xml.xpath.XPathFactory;
32  import javax.xml.xpath.XPathFactoryConfigurationException;
33  import javax.xml.xpath.XPathFunctionResolver;
34  import javax.xml.xpath.XPathVariableResolver;
35  
36  import org.junit.jupiter.api.Assumptions;
37  import org.junit.jupiter.api.Tag;
38  import org.junit.jupiter.api.Test;
39  
40  @Tag("xpath")
41  class SecureXPathFactoryTest {
42  
43      /**
44       * A processing limit the JDK's XPath implementation recognizes through the Java 18 property API.
45       */
46      private static final String XPATH_GROUP_LIMIT = "jdk.xml.xpathExprGrpLimit";
47  
48      /**
49       * The Java 18 {@code XPathFactory} property method of the given name, or an aborted test where the platform predates it.
50       *
51       * <p>
52       * Reached reflectively because this suite compiles against the Java 8 API, the same reason the wrapper delegates the pair through method handles: the
53       * call has to resolve at run time, which is also exactly how a Java 18 caller reaches it.
54       * </p>
55       */
56      private static Method propertyMethod(final String name, final Class<?>... parameterTypes) {
57          try {
58              return XPathFactory.class.getMethod(name, parameterTypes);
59          } catch (final NoSuchMethodException e) {
60              Assumptions.abort("XPathFactory." + name + " requires Java 18 or later");
61              throw new AssertionError("unreachable");
62          }
63      }
64  
65      @Test
66      void createsAndConfiguresAFactoryForTheDefaultObjectModel() throws Exception {
67          final XPathFactory factory = SecureXPathFactory.newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI);
68          final XPathFunctionResolver resolver = (name, arity) -> null;
69          factory.setXPathFunctionResolver(resolver);
70          final XPathVariableResolver variableResolver = name -> null;
71          factory.setXPathVariableResolver(variableResolver);
72          assertTrue(factory.isObjectModelSupported(XPathFactory.DEFAULT_OBJECT_MODEL_URI));
73          final SecureXPath xpath = (SecureXPath) factory.newXPath();
74          assertSame(resolver, xpath.getXPathFunctionResolver());
75          assertSame(variableResolver, xpath.getXPathVariableResolver());
76      }
77  
78      @Test
79      void createsSecureXPathFromStaticEntryPoints() {
80          assertInstanceOf(SecureXPath.class, SecureXPathFactory.newInstance().newXPath());
81          assertInstanceOf(SecureXPath.class, SecureXPathFactory.newDefaultInstance().newXPath());
82      }
83  
84      @Test
85      void delegatesTheJava18PropertyApi() throws Exception {
86          // The wrapper is compiled against the Java 8 API, so without an explicit delegation the inherited default answers for it and every property the
87          // implementation supports, including its own limits, becomes unreachable through a secured factory.
88          final Method setProperty = propertyMethod("setProperty", String.class, String.class);
89          final Method getProperty = propertyMethod("getProperty", String.class);
90          final XPathFactory factory = SecureXPathFactory.newDefaultInstance();
91          setProperty.invoke(factory, XPATH_GROUP_LIMIT, "5");
92          assertEquals("5", getProperty.invoke(factory, XPATH_GROUP_LIMIT), "a property set on the secured factory must be read back from the delegate");
93      }
94  
95      @Test
96      void preservesANullXPathFromTheDelegate() {
97          final XPathFactory delegate = new XPathFactory() {
98  
99              @Override
100             public boolean getFeature(final String name) {
101                 return false;
102             }
103 
104             @Override
105             public boolean isObjectModelSupported(final String objectModel) {
106                 return true;
107             }
108 
109             @Override
110             public XPath newXPath() {
111                 return null;
112             }
113 
114             @Override
115             public void setFeature(final String name, final boolean value) {
116             }
117 
118             @Override
119             public void setXPathFunctionResolver(final XPathFunctionResolver resolver) {
120             }
121 
122             @Override
123             public void setXPathVariableResolver(final XPathVariableResolver resolver) {
124             }
125         };
126         assertNull(SecureXPathFactory.secure(delegate).newXPath());
127     }
128 
129     @Test
130     void reportsAnUnknownPropertyLikeTheDelegate() {
131         final Method getProperty = propertyMethod("getProperty", String.class);
132         final XPathFactory factory = SecureXPathFactory.newDefaultInstance();
133         final InvocationTargetException thrown = assertThrows(InvocationTargetException.class,
134                 () -> getProperty.invoke(factory, "jdk.xml.noSuchProperty"));
135         assertInstanceOf(IllegalArgumentException.class, thrown.getCause(), "an unrecognized property must surface the delegate's own rejection");
136     }
137 
138     @Test
139     void wrapsARejectedRequiredFeatureInSecureException() {
140         final XPathFactory rejectingFactory = new XPathFactory() {
141 
142             @Override
143             public boolean getFeature(final String name) {
144                 return false;
145             }
146 
147             @Override
148             public boolean isObjectModelSupported(final String objectModel) {
149                 return true;
150             }
151 
152             @Override
153             public XPath newXPath() {
154                 return null;
155             }
156 
157             /**
158              * Always throws {@link XPathFactoryConfigurationException}.
159              *
160              * @throws XPathFactoryConfigurationException Thrown on every invocation.
161              */
162             @Override
163             public void setFeature(final String name, final boolean value) throws XPathFactoryConfigurationException {
164                 throw new XPathFactoryConfigurationException(name);
165             }
166 
167             @Override
168             public void setXPathFunctionResolver(final XPathFunctionResolver resolver) {
169             }
170 
171             @Override
172             public void setXPathVariableResolver(final XPathVariableResolver resolver) {
173             }
174         };
175         assertThrows(SecureException.class, () -> SecureXPathFactory.secure(rejectingFactory));
176     }
177 }