You are viewing a plain text version of this content. The canonical link for it is here.
Posted to jdo-commits@db.apache.org by ti...@apache.org on 2026/09/12 16:03:38 UTC

(db-jdo) 01/01: JNDI schema guard

This is an automated email from the ASF dual-hosted git repository.

tzaeschke pushed a commit to branch JNDI-scheme-guard
in repository https://gitbox.apache.org/repos/asf/db-jdo.git

commit 977c222ec554522305a27ef82b46317371b1be3f
Author: Tilmann <zo...@gmx.de>
AuthorDate: Sat Sep 12 18:03:22 2026 +0200

    JNDI schema guard
---
 api/src/main/java/javax/jdo/JDOHelper.java         | 58 ++++++++++++++++++++++
 api/src/main/resources/javax/jdo/Bundle.properties |  5 ++
 api/src/test/java/javax/jdo/JDOHelperTest.java     | 31 ++++++++++++
 3 files changed, 94 insertions(+)

diff --git a/api/src/main/java/javax/jdo/JDOHelper.java b/api/src/main/java/javax/jdo/JDOHelper.java
index aa1d0601..6268bcd1 100644
--- a/api/src/main/java/javax/jdo/JDOHelper.java
+++ b/api/src/main/java/javax/jdo/JDOHelper.java
@@ -1452,6 +1452,53 @@ public class JDOHelper implements Constants {
     }
   }
 
+  /**
+   * The name of the boolean system property that, when set to "true", allows JNDI locations with a
+   * URL scheme other than "java" (for example "ldap://..." or "rmi://...") to be passed to the
+   * JNDI-based {@link #getPersistenceManagerFactory(String, Context, ClassLoader)} overloads. Such
+   * locations are rejected by default because a URL-scheme lookup selects the naming provider from
+   * the location string itself and, depending on the JVM and provider configuration, can trigger
+   * remote class loading or deserialization of untrusted data (JNDI injection).
+   *
+   * @since 3.3
+   */
+  public static final String PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS =
+      "javax.jdo.allowUrlSchemeJndiLocations"; // NOI18N
+
+  /**
+   * Reject JNDI locations carrying a URL scheme other than "java" unless explicitly allowed via
+   * the system property named by {@link #PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS}. The JNDI
+   * location must come from trusted deployment configuration, never from request data.
+   *
+   * @param jndiLocation the JNDI location to check
+   */
+  private static void assertPermittedJndiLocation(String jndiLocation) {
+    int colon = jndiLocation.indexOf(':');
+    int slash = jndiLocation.indexOf('/');
+    if (colon <= 0 || (slash != -1 && slash < colon)) {
+      // no URL scheme: JNDI only treats "scheme:" ahead of any '/' as a URL
+      return;
+    }
+    String scheme = jndiLocation.substring(0, colon);
+    if (!scheme.matches("[A-Za-z][A-Za-z0-9+.\\-]*")) {
+      // not a syntactically valid scheme; the provider treats the location as a plain name
+      return;
+    }
+    if ("java".equalsIgnoreCase(scheme)) {
+      // local component-environment names such as "java:comp/env/jdo/PMF"
+      return;
+    }
+    if (Boolean.getBoolean(PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS)) {
+      return;
+    }
+    throw new JDOFatalUserException(
+        MSG.msg(
+            "EXC_GetPMFUrlSchemeJndiLocation", // NOI18N
+            jndiLocation,
+            scheme,
+            PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS));
+  }
+
   /**
    * Returns a {@link PersistenceManagerFactory} at the JNDI location specified by <code>
    * jndiLocation</code> in the context <code>context</code>. If <code>context</code> is <code>null
@@ -1459,6 +1506,11 @@ public class JDOHelper implements Constants {
    * {@link #getPersistenceManagerFactory(String,Context,ClassLoader)} with <code>
    * Thread.currentThread().getContextClassLoader()</code> as the <code>loader</code> argument.
    *
+   * <p><b>Security note:</b> <code>jndiLocation</code> must come from trusted deployment
+   * configuration, never from request or user data. Locations with a URL scheme other than "java"
+   * (e.g. "ldap://...") are rejected unless the system property named by {@link
+   * #PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS} is set to "true".
+   *
    * @since 2.0
    * @param jndiLocation the JNDI location containing the PersistenceManagerFactory
    * @param context the context in which to find the named PersistenceManagerFactory
@@ -1476,6 +1528,11 @@ public class JDOHelper implements Constants {
    * PersistenceManagerFactory} with <code>loader</code>. Any <code>NamingException</code>s thrown
    * will be wrapped in a {@link JDOFatalUserException}.
    *
+   * <p><b>Security note:</b> <code>jndiLocation</code> must come from trusted deployment
+   * configuration, never from request or user data. Locations with a URL scheme other than "java"
+   * (e.g. "ldap://...") are rejected unless the system property named by {@link
+   * #PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS} is set to "true".
+   *
    * @since 2.0
    * @param jndiLocation the JNDI location containing the PersistenceManagerFactory
    * @param context the context in which to find the named PersistenceManagerFactory
@@ -1487,6 +1544,7 @@ public class JDOHelper implements Constants {
     if (jndiLocation == null)
       throw new JDOFatalUserException(MSG.msg("EXC_GetPMFNullJndiLoc")); // NOI18N
     if (loader == null) throw new JDOFatalUserException(MSG.msg("EXC_GetPMFNullLoader")); // NOI18N
+    assertPermittedJndiLocation(jndiLocation);
     try {
       if (context == null) context = new InitialContext();
 
diff --git a/api/src/main/resources/javax/jdo/Bundle.properties b/api/src/main/resources/javax/jdo/Bundle.properties
index 208ef456..58b699ef 100644
--- a/api/src/main/resources/javax/jdo/Bundle.properties
+++ b/api/src/main/resources/javax/jdo/Bundle.properties
@@ -56,6 +56,11 @@ named "{0}" into a java.util.Properties object.
 EXC_GetPMFNullJndiLoc: The JNDI location argument to this method cannot be null.
 EXC_GetPMFNamingException: A NamingException was thrown while obtaining the \
 PersistenceManagerFactory at "{0}" from JNDI.
+EXC_GetPMFUrlSchemeJndiLocation: The JNDI location "{0}" uses the URL scheme "{1}". \
+URL-scheme JNDI lookups select the naming provider from the location string and can \
+trigger remote class loading or deserialization (JNDI injection), so they are \
+disabled by default. If this location is trusted deployment configuration, set the \
+system property "{2}" to "true" to allow it.
 EXC_GetPMFNullPointerException: The PersistenceManagerFactory class must define a static \
 method \nPersistenceManagerFactory getPersistenceManagerFactory(Map props). \nThe class "{0}"\n\
 defines a non-static getPersistenceManagerFactory(Map props) method.
diff --git a/api/src/test/java/javax/jdo/JDOHelperTest.java b/api/src/test/java/javax/jdo/JDOHelperTest.java
index 4da79f1a..4383f4ec 100644
--- a/api/src/test/java/javax/jdo/JDOHelperTest.java
+++ b/api/src/test/java/javax/jdo/JDOHelperTest.java
@@ -500,6 +500,37 @@ class JDOHelperTest extends AbstractTest {
     }
   }
 
+  /** Test that a URL-scheme JNDI location is rejected by default. */
+  @Test
+  void testGetPMFUrlSchemeJNDIRejected() {
+    Context context = getInitialContext();
+    JDOFatalUserException ex =
+        Assertions.assertThrows(
+            JDOFatalUserException.class,
+            () ->
+                JDOHelper.getPersistenceManagerFactory(
+                    "ldap://attacker.example:389/cn=x", context),
+            "URL-scheme JNDI location should result in JDOFatalUserException");
+    Assertions.assertTrue(
+        ex.getMessage().contains(JDOHelper.PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS),
+        "Exception should mention the opt-in system property but was: " + ex.getMessage());
+  }
+
+  /** Test that a composite-name JNDI location is not blocked by the URL-scheme check. */
+  @Test
+  void testGetPMFCompositeNameJNDINotBlockedBySchemeCheck() {
+    Context context = getInitialContext();
+    JDOFatalUserException ex =
+        Assertions.assertThrows(
+            JDOFatalUserException.class,
+            () -> JDOHelper.getPersistenceManagerFactory("java:comp/env/jdo/PMF", context),
+            "Unbound JNDI name should result in JDOFatalUserException");
+    Assertions.assertFalse(
+        ex.getMessage().contains(JDOHelper.PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS),
+        "Composite name must not be rejected by the URL-scheme check but was: "
+            + ex.getMessage());
+  }
+
   private Context getInitialContext() {
     try {
       return new InitialContext();