diff --git a/src/changes/changes.xml b/src/changes/changes.xml index c1a755b6..5542ad08 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -48,6 +48,7 @@ The type attribute can be add, update, fix, or remove. Parse a Source opted in by a caller-supplied URIResolver using a secure parser. Secure the document parse behind the InputSource-taking XPath evaluation entry points. Fall back to the standard factory lookup in the DOM, SAX and schema newDefaultInstance methods on Android. + Delegate the XPathFactory setProperty and getProperty methods introduced in Java 18, so the implementation's properties stay reachable on a secure factory. Bound the content model a schema expands into, so a compact schema with a large maxOccurs cannot exhaust memory or CPU during validation. Recognize XML implementations by the JAXP features and properties they support instead of by their implementation class name, extending the securing to any compliant implementation. diff --git a/src/main/java/org/apache/commons/xml/secure/MethodHandleFactory.java b/src/main/java/org/apache/commons/xml/secure/MethodHandleFactory.java index 75b8c996..d2fd64e2 100644 --- a/src/main/java/org/apache/commons/xml/secure/MethodHandleFactory.java +++ b/src/main/java/org/apache/commons/xml/secure/MethodHandleFactory.java @@ -49,6 +49,28 @@ static MethodHandle findStatic(final Class refcAndReturnType, final String na } } + /** + * Finds an instance method handle for the given class, method name and signature. + * + *

Used to reach a method a later Java release added to a class this library compiles against at an earlier one; the handle is {@code null} where the + * running platform does not have it.

+ * + * @param refc the class to search for the method. + * @param name the name of the method. + * @param returnType the method's return type. + * @param parameterTypes the method's parameter types. + * @return the method handle, or {@code null} if not found. + * @throws SecurityException if a security manager is present and it refuses access. + * @throws NullPointerException if any argument is null. + */ + static MethodHandle findVirtual(final Class refc, final String name, final Class returnType, final Class... parameterTypes) { + try { + return MethodHandles.publicLookup().findVirtual(refc, name, MethodType.methodType(returnType, parameterTypes)); + } catch (final ReflectiveOperationException e) { + return null; + } + } + static T invokeExact(final ThrowableCallable methodHandle, final Class rethrow) throws E { try { return methodHandle.call(); diff --git a/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java b/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java index 3c3aa9bb..5724d1d9 100644 --- a/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java +++ b/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java @@ -74,6 +74,23 @@ public boolean getFeature(final String name) throws XPathFactoryConfigurationExc return delegate.getFeature(name); } + /** + * Reports a property of the delegate, the Java 18 {@code XPathFactory.getProperty(String)}. + * + *

Not marked {@code @Override}: this library compiles against the Java 8 API, where {@link XPathFactory} declares no such method, so the annotation + * would not compile. At run time on Java 18 or later it overrides the inherited method, which would otherwise answer for the wrapper and hide the + * delegate's own limits ({@code jdk.xml.xpath*}) behind an {@code UnsupportedOperationException}.

+ * + * @param name the property name. + * @return the delegate's value for the property. + */ + public String getProperty(final String name) { + if (MH_getProperty == null) { + throw new UnsupportedOperationException("XPathFactory.getProperty(String) requires Java 18 or later"); + } + return MethodHandleFactory.invokeExact(() -> (String) MH_getProperty.invokeExact(delegate, name), RuntimeException.class); + } + @Override public boolean isObjectModelSupported(final String objectModel) { return delegate.isObjectModelSupported(objectModel); @@ -107,6 +124,24 @@ public void setFeature(final String name, final boolean value) throws XPathFacto delegate.setFeature(name, value); } + /** + * Sets a property on the delegate, the Java 18 {@code XPathFactory.setProperty(String, String)}; see {@link #getProperty(String)} for why it carries no + * {@code @Override}. The {@code jdk.xml.xpath*} limits reached this way are processing limits like any other: an operator may tighten them, and + * loosening one is reconfiguration. + * + * @param name the property name. + * @param value the value to set. + */ + public void setProperty(final String name, final String value) { + if (MH_setProperty == null) { + throw new UnsupportedOperationException("XPathFactory.setProperty(String, String) requires Java 18 or later"); + } + MethodHandleFactory.invokeExact(() -> { + MH_setProperty.invokeExact(delegate, name, value); + return null; + }, RuntimeException.class); + } + @Override public void setXPathFunctionResolver(final XPathFunctionResolver resolver) { delegate.setXPathFunctionResolver(resolver); @@ -123,6 +158,13 @@ public void setXPathVariableResolver(final XPathVariableResolver resolver) { private static final MethodHandle MH_newDefaultInstance = MethodHandleFactory.findStatic(XPathFactory.class, "newDefaultInstance"); + /** {@code XPathFactory.getProperty(String)}, added in Java 18; {@code null} on earlier releases, where the method does not exist to be called. */ + private static final MethodHandle MH_getProperty = MethodHandleFactory.findVirtual(XPathFactory.class, "getProperty", String.class, String.class); + + /** {@code XPathFactory.setProperty(String, String)}, added in Java 18; {@code null} on earlier releases, where the method does not exist to be called. */ + private static final MethodHandle MH_setProperty = + MethodHandleFactory.findVirtual(XPathFactory.class, "setProperty", void.class, String.class, String.class); + /** * Returns a new, secure {@link XPathFactory} of the system-default implementation, supporting the default XPath object model. *

diff --git a/src/test/java/org/apache/commons/xml/secure/SecureXPathFactoryTest.java b/src/test/java/org/apache/commons/xml/secure/SecureXPathFactoryTest.java index d6cbdc58..caaa5bd5 100644 --- a/src/test/java/org/apache/commons/xml/secure/SecureXPathFactoryTest.java +++ b/src/test/java/org/apache/commons/xml/secure/SecureXPathFactoryTest.java @@ -17,18 +17,23 @@ package org.apache.commons.xml.secure; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + import javax.xml.xpath.XPath; import javax.xml.xpath.XPathFactory; import javax.xml.xpath.XPathFactoryConfigurationException; import javax.xml.xpath.XPathFunctionResolver; import javax.xml.xpath.XPathVariableResolver; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -122,4 +127,42 @@ public void setXPathVariableResolver(final XPathVariableResolver resolver) { }; assertThrows(SecureException.class, () -> SecureXPathFactory.secure(rejectingFactory)); } + + /** A processing limit the JDK's XPath implementation recognizes through the Java 18 property API. */ + private static final String XPATH_GROUP_LIMIT = "jdk.xml.xpathExprGrpLimit"; + + /** + * The Java 18 {@code XPathFactory} property method of the given name, or an aborted test where the platform predates it. + * + *

Reached reflectively because this suite compiles against the Java 8 API, the same reason the wrapper delegates the pair through method handles: the + * call has to resolve at run time, which is also exactly how a Java 18 caller reaches it.

+ */ + private static Method propertyMethod(final String name, final Class... parameterTypes) { + try { + return XPathFactory.class.getMethod(name, parameterTypes); + } catch (final NoSuchMethodException e) { + Assumptions.abort("XPathFactory." + name + " requires Java 18 or later"); + throw new AssertionError("unreachable"); + } + } + + @Test + void delegatesTheJava18PropertyApi() throws Exception { + // The wrapper is compiled against the Java 8 API, so without an explicit delegation the inherited default answers for it and every property the + // implementation supports, including its own limits, becomes unreachable through a secured factory. + final Method setProperty = propertyMethod("setProperty", String.class, String.class); + final Method getProperty = propertyMethod("getProperty", String.class); + final XPathFactory factory = SecureXPathFactory.newDefaultInstance(); + setProperty.invoke(factory, XPATH_GROUP_LIMIT, "5"); + assertEquals("5", getProperty.invoke(factory, XPATH_GROUP_LIMIT), "a property set on the secured factory must be read back from the delegate"); + } + + @Test + void reportsAnUnknownPropertyLikeTheDelegate() { + final Method getProperty = propertyMethod("getProperty", String.class); + final XPathFactory factory = SecureXPathFactory.newDefaultInstance(); + final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, + () -> getProperty.invoke(factory, "jdk.xml.noSuchProperty")); + assertInstanceOf(IllegalArgumentException.class, thrown.getCause(), "an unrecognized property must surface the delegate's own rejection"); + } }