CVE-2024-36404 (Java, published 2024-07-02T14:15Z)
Description: value: GeoTools is an open source Java library that provides tools for geospatial data. Prior to versions 31.2, 30.4, and 29.6, Remote Code Execution (RCE) is possible if an application uses certain GeoTools functionality to evaluate XPath expressions supplied by user input. Versions 31.2, 30.4, and 29.6 contain a fix for this issue. As a workaround, GeoTools can operate with reduced functionality by removing the `gt-complex` jar from one's application. As an example of the impact, application schema `datastore` would not function without the ability to use XPath expressions to query complex content. Alternatively, one may utilize a drop-in replacement GeoTools jar from SourceForge for versions 31.1, 30.3, 30.2, 29.2, 28.2, 27.5, 27.4, 26.7, 26.4, 25.2, and 24.0. These jars are for download only and are not available from maven central, intended to quickly provide a fix to affected applications.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/geotools/geotools
Files changed:
modules/extension/app-schema/app-schema/src/main/java/org/geotools/appschema/util/XmlXpathUtilites.java
modules/extension/app-schema/app-schema/src/test/java/org/geotools/appschema/util/XmlXpathUtilitesTest.java
modules/extension/complex/src/main/java/org/geotools/data/complex/expression/FeaturePropertyAccessorFactory.java
modules/extension/complex/src/main/java/org/geotools/data/complex/expression/MapPropertyAccessorFactory.java
modules/extension/complex/src/test/java/org/geotools/filter/expression/FeaturePropertyAccessorTest.java
modules/extension/complex/src/test/java/org/geotools/filter/expression/MapPropertyAccessorTest.java
modules/extension/xsd/xsd-core/pom.xml
modules/extension/xsd/xsd-core/src/main/java/org/geotools/xsd/impl/jxpath/JXPathStreamingParserHandler.java
modules/extension/xsd/xsd-core/src/main/java/org/geotools/xsd/impl/jxpath/JXPathUtils.java
modules/extension/xsd/xsd-core/src/test/java/org/geotools/xsd/StreamingParserTest.java
Commit message: [GEOT-7587] Improve handling of XPath expressions


Vulnerable code:
import java.util.Enumeration;
        JXPathContext context = initialiseContext(ns, doc);
        JXPathContext context = initialiseContext(ns, doc);
            Object ob = context.getValue(xpathString);
    private static JXPathContext initialiseContext(NamespaceSupport ns, Document doc) {
        JXPathContext context = JXPathContext.newContext(doc);
        addNamespaces(ns, context);
        context.setLenient(true);
        return context;
    }

    private static void addNamespaces(NamespaceSupport ns, JXPathContext context) {
        @SuppressWarnings("unchecked")
        Enumeration<String> prefixes = ns.getPrefixes();
        while (prefixes.hasMoreElements()) {
            String prefix = prefixes.nextElement();
            String uri = ns.getURI(prefix);
            context.registerNamespace(prefix, uri);
        }
    }

import java.util.Enumeration;
        if (SimpleFeature.class.isAssignableFrom(type)) {
            JXPathContext context = JXPathContext.newContext(object);
            Enumeration declaredPrefixes = namespaces.getDeclaredPrefixes();
            while (declaredPrefixes.hasMoreElements()) {
                String prefix = (String) declaredPrefixes.nextElement();
                String uri = namespaces.getURI(prefix);
                context.registerNamespace(prefix, uri);
            }

            JXPathContext context = JXPathContext.newContext(object);
            Enumeration declaredPrefixes = namespaces.getDeclaredPrefixes();
            while (declaredPrefixes.hasMoreElements()) {
                String prefix = (String) declaredPrefixes.nextElement();
                String uri = namespaces.getURI(prefix);
                context.registerNamespace(prefix, uri);
            }
import org.apache.commons.jxpath.JXPathContext;
                    JXPathContext context = JXPathContext.newContext(object);
                    context.setLenient(true);
                    return (T) context.getValue(xpath);
      <exclusions>
        <exclusion>
          <groupId>xerces</groupId>
          <artifactId>xerces</artifactId>
        </exclusion>
        <exclusion>
          <groupId>servletapi</groupId>
          <artifactId>servletapi</artifactId>
        </exclusion>
        <exclusion>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
        </exclusion>
      </exclusions>
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.JXPathContextFactory;
        JXPathContext jxpContext = JXPathContextFactory.newInstance().newContext(null, root);

        jxpContext.setLenient(true);

        Iterator itr = jxpContext.iterate(xpath);

Fixed code:
import org.geotools.xsd.impl.jxpath.JXPathUtils;
        JXPathContext context = JXPathUtils.newSafeContext(doc, true, ns, false);
            Object ob = JXPathUtils.newSafeContext(doc, true, ns, false).getValue(xpathString);
/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2024, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.appschema.util;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;

import org.junit.Test;

public class XmlXpathUtilitesTest {

    @Test
    public void testGetXPathValuesWithJavaMethod() {
        RuntimeException exception =
                assertThrows(
                        RuntimeException.class,
                        () ->
                                XmlXpathUtilites.getXPathValues(
                                        null, "java.lang.Thread.sleep(30000)", null));
        assertEquals("Error reading xpath java.lang.Thread.sleep(30000)", exception.getMessage());
    }

    @Test
    public void testCountXPathNodesWithJavaMethod() {
        RuntimeException exception =
                assertThrows(
                        RuntimeException.class,
                        () ->
                                XmlXpathUtilites.countXPathNodes(
                                        null, "java.lang.Thread.sleep(30000)", null));
        assertEquals("Error reading xpath java.lang.Thread.sleep(30000)", exception.getMessage());
    }

    @Test
    public void testGetSingleXPathValueWithJavaMethod() {
        RuntimeException exception =
                assertThrows(
                        RuntimeException.class,
                        () ->
                                XmlXpathUtilites.getSingleXPathValue(
                                        null, "java.lang.Thread.sleep(30000)", null));
        assertEquals("Error reading xpath java.lang.Thread.sleep(30000)", exception.getMessage());
    }
}
import org.geotools.api.feature.simple.SimpleFeatureType;
import org.geotools.xsd.impl.jxpath.JXPathUtils;
        if (SimpleFeature.class.isAssignableFrom(type)
                || SimpleFeatureType.class.isAssignableFrom(type)) {
            JXPathContext context =
                    JXPathUtils.newSafeContext(object, false, this.namespaces, true);
            JXPathContext context =
                    JXPathUtils.newSafeContext(object, false, this.namespaces, true);
import org.geotools.xsd.impl.jxpath.JXPathUtils;
                    return (T) JXPathUtils.newSafeContext(object, true).getValue(xpath);

        // test that executing Java methods is blocked
        ex =
                new AttributeExpressionImpl(
                        "java.lang.Thread.sleep(30000)",
                        new Hints(FeaturePropertyAccessorFactory.NAMESPACE_CONTEXT, NAMESPACES));
        long start = System.currentTimeMillis();
        assertNull(ex.evaluate(feature));
        long runtime = System.currentTimeMillis() - start;
        assertTrue("java.lang.Thread.sleep(30000) was executed", runtime < 30000);
        start = System.currentTimeMillis();
        assertNull(ex.evaluate(fType));
        runtime = System.currentTimeMillis() - start;
        assertTrue("java.lang.Thread.sleep(30000) was executed", runtime < 30000);
/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2024, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.filter.expression;

import static org.junit.Assert.assertTrue;

import java.util.Collections;
import org.geotools.api.filter.expression.PropertyName;
import org.geotools.factory.CommonFactoryFinder;
import org.junit.Test;

public class MapPropertyAccessorTest {

    @Test
    public void testAccessWithJavaMethod() {
        PropertyName property =
                CommonFactoryFinder.getFilterFactory().property("java.lang.Thread.sleep(30000)");
        long start = System.currentTimeMillis();
        property.evaluate(Collections.emptyMap());
        long runtime = System.currentTimeMillis() - start;
        assertTrue("java.lang.Thread.sleep(30000) was executed", runtime < 30000);
    }
}
        Iterator itr = JXPathUtils.newSafeContext(root, true).iterate(xpath);
/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2024, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.xsd.impl.jxpath;

import java.util.Enumeration;
import org.apache.commons.jxpath.FunctionLibrary;
import org.apache.commons.jxpath.JXPathContext;
import org.xml.sax.helpers.NamespaceSupport;

/** Contains utility methods to create safe {@code JXPathContext} objects. */
public class JXPathUtils {

    /**
     * Creates a {@code JXPathContext} that disables calling Java methods from XPath expressions.
     *
     * @param contextBean the root node object
     * @param lenient whether the context is in lenient mode
     * @return the context
     */
    public static JXPathContext newSafeContext(Object contextBean, boolean lenient) {
        return newSafeContext(contextBean, lenient, null, true);
    }

    /**
     * Creates a {@code JXPathContext} that disables calling Java methods from XPath expressions.
     *
     * @param contextBean the root node object
     * @param lenient whether the context is in lenient mode
     * @param ns the namespaces
     * @param declared true to include all declared prefixes; false for all active prefixes
     * @return the context
     */
    @SuppressWarnings("unchecked")
    public static JXPathContext newSafeContext(
            Object contextBean, boolean lenient, NamespaceSupport ns, boolean declared) {
        JXPathContext context = JXPathContext.newContext(contextBean);
        context.setLenient(lenient);
        if (ns != null) {
            Enumeration<String> prefixes = declared ? ns.getDeclaredPrefixes() : ns.getPrefixes();
            while (prefixes.hasMoreElements()) {
                String prefix = prefixes.nextElement();
                String uri = ns.getURI(prefix);
                context.registerNamespace(prefix, uri);
            }
        }
        // Set empty function library to prevent calling functions
        context.setFunctions(new FunctionLibrary());
        return context;
    }
}
import static org.junit.Assert.assertTrue;

    @Test
    public void testParseWithJavaMethod() throws Exception {
        ByteArrayInputStream in = new ByteArrayInputStream("<mails></mails>".getBytes());
        StreamingParser parser =
                new StreamingParser(new MLConfiguration(), in, "java.lang.Thread.sleep(30000)");
        // StreamingParser returns null if the parsing fails
        long start = System.currentTimeMillis();
        assertNull(parser.parse());
        long runtime = System.currentTimeMillis() - start;
        assertTrue("java.lang.Thread.sleep(30000) was executed", runtime < 30000);
    }