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();