From b18db4e0be2fe0c61d97ddf156353f1930293aff Mon Sep 17 00:00:00 2001 From: Julian Reschke Date: Tue, 6 Oct 2026 08:42:53 +0100 Subject: [PATCH] jcr2dav: Uncaught non-RepositoryException types in HTTP responses break JCR clients --- .../spi2dav/ExceptionConverter.java | 126 ++++++++++++++++-- 1 file changed, 112 insertions(+), 14 deletions(-) diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/ExceptionConverter.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/ExceptionConverter.java index e0346c7bebb..81c7cf7de1b 100644 --- a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/ExceptionConverter.java +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/ExceptionConverter.java @@ -17,6 +17,8 @@ package org.apache.jackrabbit.spi2dav; import java.lang.reflect.Constructor; +import java.util.HashSet; +import java.util.Set; import javax.jcr.InvalidItemStateException; import javax.jcr.ItemNotFoundException; @@ -35,23 +37,88 @@ import org.w3c.dom.Element; /** - * ExceptionConverter... + * Converter utility for mapping WebDAV protocol errors ({@link DavException}) into + * corresponding JCR API exceptions ({@link RepositoryException}). + *

+ * This class translates WebDAV server responses back into native JCR exception types so that + * remote repository client operations maintain standard JCR API error behaviors. + *

*/ public class ExceptionConverter { + // these exception classes are allowed to be instantiated + private static final Set SAFE_EXCEPTION_NAMES = new HashSet<>(); + static { + SAFE_EXCEPTION_NAMES.add(javax.jcr.AccessDeniedException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.InvalidItemStateException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.InvalidLifecycleTransitionException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.InvalidSerializedDataException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.ItemExistsException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.ItemNotFoundException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.LoginException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.MergeException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.NamespaceException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.NoSuchWorkspaceException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.PathNotFoundException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.ReferentialIntegrityException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.RepositoryException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.UnsupportedRepositoryOperationException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.ValueFormatException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.lock.LockException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.nodetype.ConstraintViolationException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.nodetype.InvalidNodeTypeDefinitionException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.nodetype.NoSuchNodeTypeException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.nodetype.NodeTypeExistsException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.query.InvalidQueryException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.security.AccessControlException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.version.ActivityViolationException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.version.LabelExistsVersionException.class.getName()); + SAFE_EXCEPTION_NAMES.add(javax.jcr.version.VersionException.class.getName()); + } + // avoid instantiation private ExceptionConverter() {} + /** + * @param davExc the WebDAV exception to convert + * @return the mapped JCR {@link RepositoryException} + * @see #generate(DavException, HttpRequestBase) + */ public static RepositoryException generate(DavException davExc) { return generate(davExc, null); } + /** + * @param davExc the WebDAV exception to convert + * @param request the HTTP request that triggered the WebDAV exception, or {@code null} if unavailable + * @return the mapped JCR {@link RepositoryException} + * @see #generate(DavException, int, String) + */ public static RepositoryException generate(DavException davExc, HttpRequestBase request) { String name = (request == null) ? "_undefined_" : request.getMethod(); int code = DavMethods.getMethodCode(name); return generate(davExc, code, name); } + /** + * Generates a JCR {@link RepositoryException} from a {@link DavException}, HTTP method code, + * and method name string. + *

+ * The method first attempts to extract explicit exception information (class name and message) + * from the WebDAV XML error response. If the class name matches an entry in {@link #SAFE_EXCEPTION_NAMES}, + * it reflectively instantiates that specific JCR exception. + *

+ *

+ * If no XML error details exist or the exception class is not in the allow-list, it converts + * the HTTP status code (and method context where appropriate) into a standard JCR exception type + * such as {@link ItemNotFoundException}, {@link LockException}, or {@link ConstraintViolationException}. + *

+ * + * @param davExc the WebDAV exception to convert + * @param methodCode the numeric code of the WebDAV HTTP method (from {@link DavMethods}) + * @param name the HTTP method name string (e.g., "GET", "POST", "MKCOL") + * @return the resulting JCR {@link RepositoryException} + */ public static RepositoryException generate(DavException davExc, int methodCode, String name) { String msg = davExc.getMessage(); if (davExc.hasErrorCondition()) { @@ -64,18 +131,16 @@ public static RepositoryException generate(DavException davExc, int methodCode, msg = DomUtil.getChildText(exc, "message", null); } if (DomUtil.hasChildElement(exc, "class", null)) { - Class cl = Class.forName(DomUtil.getChildText(exc, "class", null)); - Constructor excConstr = cl.getConstructor(String.class); - if (excConstr != null) { - Object o = excConstr.newInstance(msg); - if (o instanceof PathNotFoundException && methodCode == DavMethods.DAV_POST) { - // see JCR-2536 - return new InvalidItemStateException(msg); - } else if (o instanceof RepositoryException) { - return (RepositoryException) o; - } else if (o instanceof Exception) { - return new RepositoryException(msg, (Exception)o); - } + String className = DomUtil.getChildText(exc, "class", null); + // never Class.forName or construct on wire data: + // only the standard JCR exception types are + // reconstructed; any other class name falls + // through to the status-code based mapping below. + if (className != null && SAFE_EXCEPTION_NAMES.contains(className)) { + return getRepositoryException(methodCode, className, msg); + } else { + throw new InvalidItemStateException( + String.format("Class '%s' not in allow list for exception conversion", className)); } } } @@ -112,7 +177,7 @@ public static RepositoryException generate(DavException davExc, int methodCode, if (methodCode > 0 && name != null) { return new UnsupportedRepositoryOperationException( "Missing implementation: Method " - + name + " could not be executed", davExc); + + name + " could not be executed", davExc); } else { return new UnsupportedRepositoryOperationException( "Missing implementation", davExc); @@ -121,4 +186,37 @@ public static RepositoryException generate(DavException davExc, int methodCode, return new RepositoryException(msg, davExc); } } + + /** + * Reflectively instantiates a {@link RepositoryException} sub-class for the given class name and error message. + *

+ * Performs a type-check to confirm that the requested class extends {@link RepositoryException}. + * Special handling is applied for {@link PathNotFoundException} when encountered during a {@code POST} request, + * translating it into an {@link InvalidItemStateException}. + *

+ * + * @param methodCode the numeric code of the WebDAV HTTP method + * @param className the fully qualified class name of the target JCR exception + * @param msg the error detail message + * @return the reflectively created {@link RepositoryException} + * @throws ReflectiveOperationException if class loading, constructor resolution, or instantiation fails + * @throws InvalidItemStateException if the target class is not a subclass of {@link RepositoryException} + */ + private static RepositoryException getRepositoryException(int methodCode, String className, String msg) throws ReflectiveOperationException, InvalidItemStateException { + Class cl = Class.forName(className); + // type check before construction + if (RepositoryException.class.isAssignableFrom(cl)) { + Constructor excConstr = cl.getConstructor(String.class); + Object o = excConstr.newInstance(msg); + if (o instanceof PathNotFoundException && methodCode == DavMethods.DAV_POST) { + // see JCR-2536 + return new InvalidItemStateException(msg); + } else { + return (RepositoryException) o; + } + } else { + throw new InvalidItemStateException( + String.format("Class '%s' not assignable from '%s'", RepositoryException.class.getName(), className)); + } + } }