Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ The <action> type attribute can be add, update, fix, or remove.
<action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory">Parse a Source opted in by a caller-supplied URIResolver using a secure parser.</action>
<action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory">Secure the document parse behind the InputSource-taking XPath evaluation entry points.</action>
<action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory">Fall back to the standard factory lookup in the DOM, SAX and schema newDefaultInstance methods on Android.</action>
<action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory">Delegate the XPathFactory setProperty and getProperty methods introduced in Java 18, so the implementation's properties stay reachable on a secure factory.</action>
<action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory">Bound the content model a schema expands into, so a compact schema with a large maxOccurs cannot exhaust memory or CPU during validation.</action>
<!-- UPDATE -->
<action type="update" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory" issue="COMMONSXML-1,COMMONSXML-5,COMMONSXML-6,COMMONSXML-7,COMMONSXML-8">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.</action>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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.</p>
*
* @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 <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>.
* @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, E extends Throwable> T invokeExact(final ThrowableCallable<T> methodHandle, final Class<E> rethrow) throws E {
try {
return methodHandle.call();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)}.
*
* <p>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}.</p>
*
* @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);
Expand Down Expand Up @@ -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);
Expand All @@ -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.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
*
* <p>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.</p>
*/
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");
}
}
Loading